Open32

Seaport Protocol を試す

odanodan
import { Seaport } from "seaport-js";
import { ethers } from "ethers";

async function main(): Promise<void> {
  const provider = ethers.getDefaultProvider();

  const seaport = new Seaport(provider);
}

main();

README に乗ってるコードをコピペしてきてもエラーになる...

odanodan

第二引数を空の引数にしてもエラーになる...

odanodan

動かないなあと思ったら

import { SeaportSDK } from "seaport-js";

が正しい

odanodan

↑のインタフェースの変更は 1.1.0 かららしく、1.0.2 を試す
ちなみに 1.1.0 のソースコードは GitHub に push されていない

odanodan

と思ったらそんなバージョンはなかった...

odanodan

あー今気づいたけど @opensea/seaport-jsseaport-js は別らしい...

odanodan

opensea-js からリスティングしてみた

https://testnets.opensea.io/assets/rinkeby/0x344f2e83f4f6c5d85f3c3c0e1293c0b5c3b100f7/0

トランザクションの一覧を見ると setApprovalForAll のトランザクションしかない

opensea-js @opensea/seaport-js の実装を見るに

  • @opensea/seaport-js で order の署名をする
  • opensea-js で OpenSea のバックエンドに署名を送信

という役割分担になっているっぽい

https://github.com/ProjectOpenSea/opensea-js/blob/ffae9dbaa0cdd72a4911081db05fae6dfd86dd22/src/sdk.ts#L1106

odanodan

シグネチャは 0xfb0f3ee1 で呼び出されたメソッドは fulfillBasicOrder っぽい

デフォルトでは @opensea/seaport-jsNO_CONDUITconduitKey に指定するので、Seaport だけで転送が行われる

odanodan

@opensea/seaport-js が ethers と相性が悪い理由がわかった

https://twitter.com/odan3240/status/1540561445783384064

JsonRpcProvidergetSignerJsonRpcSigner が返されてしまうので、Wallet クラスのインスタンスを provider と紐付けていても意味がない
https://github.com/ethers-io/ethers.js/blob/6807a76b8ddfdd121f98b21e269de3b195a7673e/packages/providers/src.ts/json-rpc-provider.ts#L408-L410

JsonRpcSigner はトランザクションを投げる場合、this.provider.send("eth_sendTransaction" してしまうのでローカルにある秘密鍵を使えない
https://github.com/ethers-io/ethers.js/blob/6807a76b8ddfdd121f98b21e269de3b195a7673e/packages/providers/src.ts/json-rpc-provider.ts#L212

odanodan

@opensea/seaport-js が ethers と仲良くするには Signer をコンストラクタで受け取らないといけない

今の provider に依存する仕組みだと @truffle/hdwallet-provider などの eth_sendTransaction に hook してトランザクションを投げる provider も使う必要がある

odanodan
import { Seaport } from "@opensea/seaport-js";
import { ItemType } from "@opensea/seaport-js/lib/constants";
import { ethers } from "ethers";
import HDWalletProvider from "@truffle/hdwallet-provider";
import { OpenSeaAPI } from "opensea-js";

const RPC_URL = process.env["RPC_URL"]!;
const PRIVATE_KEY = process.env["PRIVATE_KEY"]!;

async function main(): Promise<void> {
  const api = new OpenSeaAPI({});
  const hdWalletProvider = new HDWalletProvider({
    privateKeys: [PRIVATE_KEY],
    providerOrUrl: RPC_URL,
  });

  const provider = new ethers.providers.Web3Provider(hdWalletProvider);
  const address = await provider.getSigner().getAddress();

  const seaport = new Seaport(provider, {
    conduitKeyToConduit: {
      "0xA76F9817813C745DC9B678A7363DC774B36DE0D9000000000000000000000000":
        "0xf3f8b809f0313b270b825642f106149cdc26cd09",
    },
  });

  const { executeAllActions } = await seaport.createOrder(
    {
      offer: [
        {
          itemType: ItemType.ERC721,
          token: "0xa499f7d121dd0daa7e65b273a773cdca45a68cb0",
          identifier: "0",
        },
      ],
      consideration: [
        {
          amount: ethers.utils.parseEther("0.000001").toString(),
          recipient: address,
        },
      ],
      conduitKey:
        "0xA76F9817813C745DC9B678A7363DC774B36DE0D9000000000000000000000000",
    },
    address
  );

  const order = await executeAllActions();
  console.log("order", order);

  const orderV2 = await api.postOrder(order, {
    protocol: "seaport",
    side: "ask",
  });
  console.log("orderV2", orderV2);
}

main();

process.on("unhandledRejection", (reason) => {
  console.error(reason);
  process.exit(1);
});

ためしに投げてみるとエラーになった

Error: API Error 400: You provided an invalid conduit key: 0xA76F9817813C745DC9B678A7363DC774B36DE0D9000000000000000000000000, please use OpenSea's conduit key: 0x0000007b02230091a7ed01230072f7006a004d60a8d4e71d599b8104250f0000 for order placement

0xA76F9817813C745DC9B678A7363DC774B36DE0D9000000000000000000000000 は使えなくて 0x0000007b02230091a7ed01230072f7006a004d60a8d4e71d599b8104250f0000 しかだめらしい

odanodan
import { Seaport } from "@opensea/seaport-js";
import { ItemType } from "@opensea/seaport-js/lib/constants";
import { ethers } from "ethers";
import HDWalletProvider from "@truffle/hdwallet-provider";
import { OpenSeaAPI } from "opensea-js";
import {
  CONDUIT_KEYS_TO_CONDUIT,
  CROSS_CHAIN_DEFAULT_CONDUIT_KEY,
} from "../node_modules/opensea-js/lib/constants";

const RPC_URL = process.env["RPC_URL"]!;
const PRIVATE_KEY = process.env["PRIVATE_KEY"]!;

async function main(): Promise<void> {
  const api = new OpenSeaAPI({});
  const hdWalletProvider = new HDWalletProvider({
    privateKeys: [PRIVATE_KEY],
    providerOrUrl: RPC_URL,
  });

  const provider = new ethers.providers.Web3Provider(hdWalletProvider);
  const address = await provider.getSigner().getAddress();

  const seaport = new Seaport(provider, {
    conduitKeyToConduit: CONDUIT_KEYS_TO_CONDUIT,
  });

  const { executeAllActions } = await seaport.createOrder(
    {
      offer: [
        {
          itemType: ItemType.ERC721,
          token: "0xa499f7d121dd0daa7e65b273a773cdca45a68cb0",
          identifier: "0",
        },
      ],
      consideration: [
        {
          amount: ethers.utils.parseEther("0.000001").toString(),
          recipient: address,
        },
      ],
      conduitKey: CROSS_CHAIN_DEFAULT_CONDUIT_KEY,
    },
    address
  );

  const order = await executeAllActions();
  console.log("order", order);

  const orderV2 = await api.postOrder(order, {
    protocol: "seaport",
    side: "ask",
  });
  console.log("orderV2", orderV2);
}

main();

process.on("unhandledRejection", (reason) => {
  console.error(reason);
  process.exit(1);
});

修正したけどエラーがでた

Error: API Error 400: You provided an invalid zone: 0x0000000000000000000000000000000000000000, please use OpenSea's zone: 0x004c00500000ad104d7dbd00e3ae0a5c00560c00 for order placement

zone、とは...?

odanodan

PausableZone ってやつらしい
https://etherscan.io/address/0x004c00500000ad104d7dbd00e3ae0a5c00560c00#code

必要なインタフェースは次の通り
https://github.com/ProjectOpenSea/seaport/blob/891b5d4f52b58eb7030597fbb22dca67fd86c4c8/contracts/interfaces/ZoneInterface.sol#L10-L27

validator を横から差し込める君っぽい

PausableZone は緊急時にマーケットの取引を止める君っぽい?

pause すると selfdestruct する仕組みらしく、pause 後は↓の判定が false になるって感じかな
https://github.com/ProjectOpenSea/seaport/blob/729db312aedc7427477bd7d99232c9bb24fa5448/reference/lib/ReferenceZoneInteraction.sol#L49-L58

odanodan
import { Seaport } from "@opensea/seaport-js";
import { ItemType } from "@opensea/seaport-js/lib/constants";
import { ethers } from "ethers";
import HDWalletProvider from "@truffle/hdwallet-provider";
import { OpenSeaAPI } from "opensea-js";
import {
  CONDUIT_KEYS_TO_CONDUIT,
  CROSS_CHAIN_DEFAULT_CONDUIT_KEY,
  DEFAULT_ZONE_BY_NETWORK,
} from "../node_modules/opensea-js/lib/constants";
import { Network } from "../node_modules/opensea-js/lib/types";

const RPC_URL = process.env["RPC_URL"]!;
const PRIVATE_KEY = process.env["PRIVATE_KEY"]!;

async function main(): Promise<void> {
  const api = new OpenSeaAPI({});
  const hdWalletProvider = new HDWalletProvider({
    privateKeys: [PRIVATE_KEY],
    providerOrUrl: RPC_URL,
  });

  const provider = new ethers.providers.Web3Provider(hdWalletProvider);
  const address = await provider.getSigner().getAddress();

  const seaport = new Seaport(provider, {
    conduitKeyToConduit: CONDUIT_KEYS_TO_CONDUIT,
  });

  const { executeAllActions } = await seaport.createOrder(
    {
      offer: [
        {
          itemType: ItemType.ERC721,
          token: "0xa499f7d121dd0daa7e65b273a773cdca45a68cb0",
          identifier: "0",
        },
      ],
      consideration: [
        {
          amount: ethers.utils.parseEther("0.000001").toString(),
          recipient: address,
        },
      ],
      conduitKey: CROSS_CHAIN_DEFAULT_CONDUIT_KEY,
      zone: DEFAULT_ZONE_BY_NETWORK[Network.Rinkeby],
    },
    address
  );

  const order = await executeAllActions();
  console.log("order", order);

  const orderV2 = await api.postOrder(order, {
    protocol: "seaport",
    side: "ask",
  });
  console.log("orderV2", orderV2);
}

main();

process.on("unhandledRejection", (reason) => {
  console.error(reason);
  process.exit(1);
});

変更したけどエラーが出た

Error: API Error 400: You provided an invalid order type: OrderType.FULL_OPEN, please set it to be FULL_RESTRICTED or PARTIAL_RESTRICTED

パラメータが変なのかな

odanodan

createOrder

      restrictedByZone: true,
      allowPartialFills: true,

を指定すると別のエラーが出た

Error: API Error 400: You have attempted to create a listing, but you provided an invalid number of consideration items: 1. A consideration array for a listing may only have 2 or 3 items
odanodan
import { Seaport } from "@opensea/seaport-js";
import { ItemType } from "@opensea/seaport-js/lib/constants";
import { ethers } from "ethers";
import HDWalletProvider from "@truffle/hdwallet-provider";
import { OpenSeaAPI } from "opensea-js";
import {
  CONDUIT_KEYS_TO_CONDUIT,
  CROSS_CHAIN_DEFAULT_CONDUIT_KEY,
  DEFAULT_ZONE_BY_NETWORK,
} from "../node_modules/opensea-js/lib/constants";
import { Network } from "../node_modules/opensea-js/lib/types";

const RPC_URL = process.env["RPC_URL"]!;
const PRIVATE_KEY = process.env["PRIVATE_KEY"]!;

async function main(): Promise<void> {
  const api = new OpenSeaAPI({ networkName: Network.Rinkeby });
  const hdWalletProvider = new HDWalletProvider({
    privateKeys: [PRIVATE_KEY],
    providerOrUrl: RPC_URL,
  });

  const provider = new ethers.providers.Web3Provider(hdWalletProvider);
  const address = await provider.getSigner().getAddress();

  const seaport = new Seaport(provider, {
    conduitKeyToConduit: CONDUIT_KEYS_TO_CONDUIT,
  });

  const { executeAllActions } = await seaport.createOrder(
    {
      offer: [
        {
          itemType: ItemType.ERC721,
          token: "0xa499f7d121dd0daa7e65b273a773cdca45a68cb0",
          identifier: "0",
          amount: "1",
        },
      ],
      consideration: [
        {
          amount: ethers.utils.parseEther("0.000001").toString(),
          recipient: address,
        },
        {
          amount: ethers.utils.parseEther("0.000001").toString(),
          recipient: address,
        },
      ],
      endTime: Math.ceil(
        (new Date().getTime() + 10 * 24 * 60 * 60 * 1000) / 1000
      ).toString(),
      conduitKey: CROSS_CHAIN_DEFAULT_CONDUIT_KEY,
      zone: DEFAULT_ZONE_BY_NETWORK[Network.Rinkeby],
      restrictedByZone: true,
      allowPartialFills: true,
    },
    address
  );

  const order = await executeAllActions();
  console.log("order", order);

  const orderV2 = await api.postOrder(order, {
    protocol: "seaport",
    side: "ask",
  });
  console.log("orderV2", orderV2);
}

main();

process.on("unhandledRejection", (reason) => {
  console.error(reason);
  process.exit(1);
});

適当に指定すると怒られる

Error: API Error 400: OpenSea fees are required to post this order.

OpenSea への fee を含めることを必須にしている
二次流通時の還元を必須化するマケプレとかも作れそう

odanodan
      consideration: [
        {
          amount: amount.toString(),
        },
        {
          amount: amount
            .mul(DEFAULT_SELLER_FEE_BASIS_POINTS)
            .div(INVERSE_BASIS_POINT)
            .toString(),
          recipient: OPENSEA_FEE_RECIPIENT,
        },
      ],

considerationOPENSEA_FEE_RECIPIENT を指定しても同じエラーがでる
なんでやろ...