手把手带你构建Sui的索引器及RPC服务 @SUI Move开发必知必会

  • rzexin
  • 更新于 2024-06-27 23:48
  • 阅读 1550

手把手带你构建Sui的索引器及RPC服务 @SUI Move开发必知必会

手把手带你构建Sui的索引器及RPC服务 @SUI Move开发必知必会

1 前言

索引器(indexer 是从链上获取数据,进过处理后,存储到链下数据库,并提供API接口供查询的服务。

使用索引器的好处:

  • 提高查询效率:通过索引器将链上数据存储在本地数据库中,可以利用数据库的索引和丰富的查询功能,提高查询效率
  • 降低节点负担:如果每个应用都直接从区块链节点查询数据,将给节点带来巨大的负担。通过在本地数据库中存储链上数据,可以减轻节点的压力,提高整个网络的稳定性
  • 数据筛选和处理:区块链上的数据通常是原始的、未经处理的。通过将数据存储在本地数据库中,可以对数据进行筛选、处理和分析,提供更加符合应用需求的API服务
  • 灵活性和扩展性:通过提供API服务,可以更方便地对接各种应用和服务,提高应用的灵活性和扩展性

系列文章目录:

2 索引器构建

2.1 工程创建

$ mkdir indexer
$ pnpm init
$ pnpm add typescript prisma ts-node  @types/node -D
$ pnpm add @mysten/sui @prisma/client

2.2 代码实现

2.2.1 数据模型定义

之所以采用sqlite是一个文件数据库,是为了方便使用,不需要额外部署其它数据库服务,但查询的能力会受限。

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider = "prisma-client-js"
}

/// We can setup the provider to our database
/// For this DEMO, we're using sqlite, which allows us to not
/// have external dependencies.
datasource db {
  provider = "sqlite"
  url      = "file:./dev.db"
}

/// Our `Locked` objects list
model Locked {
  // Keeping an ID so we can use as a pagination cursor
  // There's an issue with BigInt for sqlite, so we're using a plain ID.
  id Int @id @default(autoincrement())
  objectId String @unique
  keyId String?
  creator String?
  itemId String?
  deleted Boolean @default(false)

  @@index([creator])
  @@index([deleted])
}

/// Our swap objects list
model Escrow {
  // Keeping an ID so we can use as a pagination cursor
  // There's an issue with BigInt for sqlite, so we're using a plain ID.
  id Int @id @default(autoincrement())
  objectId String @unique
  sender String?
  recipient String?
  keyId String?
  itemId String?
  swapped Boolean @default(false)
  cancelled Boolean @default(false)

  @@index([recipient])
  @@index([sender])
}

/// Saves the latest cursor for a given key.
model Cursor {
  id String @id
  eventSeq String
  txDigest String
}
  • 执行数据迁移命令
$ npx prisma migrate dev --name init

Prisma schema loaded from prisma/schema.prisma
Datasource "db": SQLite database "dev.db" at "file:./dev.db"

SQLite database dev.db created at file:./dev.db

Applying migration `20240623141512_init`

The following migration(s) have been created and applied from new schema changes:

migrations/
  └─ 20240623141512_init/
    └─ migration.sql

Your database is now in sync with your schema.

✔ Generated Prisma Client (v5.15.1) to ./node_modules/.pnpm/@prisma+client@5.15.1_prisma@5.15.1/node_modules/@prisma/client in 46ms
  • 查看到生成的数据库建表语句

这里只是prisma工具迁移操作会生成使用的建表语句,但实际上不需要用户去执行以下语句。

CREATE TABLE "Locked" (
    "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    "objectId" TEXT NOT NULL,
    "keyId" TEXT,
    "creator" TEXT,
    "itemId" TEXT,
    "deleted" BOOLEAN NOT NULL DEFAULT false
);

-- CreateTable
CREATE TABLE "Escrow" (
    "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    "objectId" TEXT NOT NULL,
    "sender" TEXT,
    "recipient" TEXT,
    "keyId" TEXT,
    "itemId" TEXT,
    "swapped" BOOLEAN NOT NULL DEFAULT false,
    "cancelled" BOOLEAN NOT NULL DEFAULT false
);

-- CreateTable
CREATE TABLE "Cursor" (
    "id" TEXT NOT NULL PRIMARY KEY,
    "eventSeq" TEXT NOT NULL,
    "txDigest" TEXT NOT NULL
);

-- CreateIndex
CREATE UNIQUE INDEX "Locked_objectId_key" ON "Locked"("objectId");

-- CreateIndex
CREATE INDEX "Locked_creator_idx" ON "Locked"("creator");

-- CreateIndex
CREATE INDEX "Locked_deleted_idx" ON "Locked"("deleted");

-- CreateIndex
CREATE UNIQUE INDEX "Escrow_objectId_key" ON "Escrow"("objectId");

-- CreateIndex
CREATE INDEX "Escrow_recipient_idx" ON "Escrow"("recipient");

-- CreateIndex
CREATE INDEX "Escrow_sender_idx" ON "Escrow"("sender");

2.2.2 定义关注事件及其回调

将会监听以下两个模块产生的事件,可以在这里查看到具体的合约实现:https://github.com/MystenLabs/sui/tree/mainnet-v1.27.2/examples/trading/contracts/escrow/sources

  • lock
  • shared
type EventTracker = {
    // The module that defines the type, with format `package::module`
    type: string;
    filter: SuiEventFilter;
    callback: (events: SuiEvent[], type: string) => any;
};

const EVENTS_TO_TRACK: EventTracker[] = [
    {
        type: `${CONFIG.SWAP_CONTRACT.packageId}::lock`,
        filter: {
            MoveEventModule: {
                module: 'lock',
                package: CONFIG.SWAP_CONTRACT.packageId,
            },
        },
        callback: handleLockObjects,
    },
    {
        type: `${CONFIG.SWAP_CONTRACT.packageId}::shared`,
        filter: {
            MoveEventModule: {
                module: 'shared',
                package: CONFIG.SWAP_CONTRACT.packageId,
            },
        },
        callback: handleEscrowObjects,
    },
];

2.2.3 处理lock合约事件方法

  • 定义lock事件类型
type LockEvent = LockCreated | LockDestroyed;

type LockCreated = {
  creator: string;
  lock_id: string;
  key_id: string;
  item_id: string;
};

type LockDestroyed = {
  lock_id: string;
};
  • lock事件处理方法实现
export const handleLockObjects = async (events: SuiEvent[], type: string) => {
  const updates: Record<string, Prisma.LockedCreateInput> = {};

  for (const event of events) {
    if (!event.type.startsWith(type))
      throw new Error("Invalid event module origin");

    const data = event.parsedJson as LockEvent;
    const isDeletionEvent = !("key_id" in data);

    if (!Object.hasOwn(updates, data.lock_id)) {
      updates[data.lock_id] = {
        objectId: data.lock_id,
      };
    }

    // Handle deletion
    if (isDeletionEvent) {
      updates[data.lock_id].deleted = true;
      continue;
    }

    // Handle creation event
    updates[data.lock_id].keyId = data.key_id;
    updates[data.lock_id].creator = data.creator;
    updates[data.lock_id].itemId = data.item_id;
  }

  //  As part of the demo and to avoid having external dependencies, we use SQLite as our database.
  //    Prisma + SQLite does not support bulk insertion & conflict handling, so we have to insert these 1 by 1
  //    (resulting in multiple round-trips to the database).
  //  Always use a single `bulkInsert` query with proper `onConflict` handling in production databases (e.g Postgres)
  const promises = Object.values(updates).map((update) =>
    prisma.locked.upsert({
      where: {
        objectId: update.objectId,
      },
      create: {
        ...update,
      },
      update,
    })
  );
  await Promise.all(promises);
};

2.2.4 处理shared合约事件方法

  • 定义事件类型
type EscrowEvent = EscrowCreated | EscrowCancelled | EscrowSwapped;

type EscrowCreated = {
  sender: string;
  recipient: string;
  escrow_id: string;
  key_id: string;
  item_id: string;
};

type EscrowSwapped = {
  escrow_id: string;
};

type EscrowCancelled = {
  escrow_id: string;
};
  • 托管事件处理方法实现
export const handleEscrowObjects = async (events: SuiEvent[], type: string) => {
  const updates: Record<string, Prisma.EscrowCreateInput> = {};

  for (const event of events) {
    if (!event.type.startsWith(type))
      throw new Error("Invalid event module origin");

    const data = event.parsedJson as EscrowEvent;

    if (!Object.hasOwn(updates, data.escrow_id)) {
      updates[data.escrow_id] = {
        objectId: data.escrow_id,
      };
    }

    // Escrow cancellation case
    if (event.type.endsWith("::EscrowCancelled")) {
      const data = event.parsedJson as EscrowCancelled;
      updates[data.escrow_id].cancelled = true;
      continue;
    }

    // Escrow swap case
    if (event.type.endsWith("::EscrowSwapped")) {
      const data = event.parsedJson as EscrowSwapped;
      updates[data.escrow_id].swapped = true;
      continue;
    }

    const creationData = event.parsedJson as EscrowCreated;

    // Handle creation event
    updates[data.escrow_id].sender = creationData.sender;
    updates[data.escrow_id].recipient = creationData.recipient;
    updates[data.escrow_id].keyId = creationData.key_id;
    updates[data.escrow_id].itemId = creationData.item_id;
  }

  //  As part of the demo and to avoid having external dependencies, we use SQLite as our database.
  //    Prisma + SQLite does not support bulk insertion & conflict handling, so we have to insert these 1 by 1
  //    (resulting in multiple round-trips to the database).
  //  Always use a single `bulkInsert` query with proper `onConflict` handling in production databases (e.g Postgres)
  const promises = Object.values(updates).map((update) =>
    prisma.escrow.upsert({
      where: {
        objectId: update.objectId,
      },
      create: update,
      update,
    })
  );
  await Promise.all(promises);
};

2.2.5 执行索引器

  • 索引器会遍历我们关注的事件(EVENTS_TO_TRACK),并未每个事件调用runEventJob方法,该方法会从上一次订阅结束的位置(getLatestCursor)开始订阅

    /// They are polling the RPC endpoint every second.
    export const setupListeners = async () => {
      for (const event of EVENTS_TO_TRACK) {
        runEventJob(getClient(CONFIG.NETWORK), event, await getLatestCursor(event));
      }
    };
    
    /**
     * Gets the latest cursor for an event tracker, either from the DB (if it's undefined)
     *  or from the running cursors.
     */
    const getLatestCursor = async (tracker: EventTracker) => {
      const cursor = await prisma.cursor.findUnique({
        where: {
          id: tracker.type,
        },
      });
    
      return cursor || undefined;
    };
  • 将递归循环调用事件订阅函数,订阅关注事件,一轮订阅完成后,如果还有没处理完的事件会立即开始新一轮订阅,否则休眠1s后,在此开始新一轮订阅

    type SuiEventsCursor = EventId | null | undefined;
    
    const runEventJob = async (
      client: SuiClient,
      tracker: EventTracker,
      cursor: SuiEventsCursor
    ) => {
      const result = await executeEventJob(client, tracker, cursor);
    
      // Trigger a timeout. Depending on the result, we either wait 0ms or the polling interval.
      setTimeout(
        () => {
          runEventJob(client, tracker, result.cursor);
        },
        result.hasNextPage ? 0 : CONFIG.POLLING_INTERVAL_MS
      );
    };
  • 执行事件订阅

    • 调用queryEvents方法,采用递增方式分页去获取事件

    • 订阅到数据后,进行回调,更新到数据库

    • 如果获取到了订阅数据,便会更新最近订阅游标

    • 返回最新的游标以及是否还有下一页布尔值

    type EventExecutionResult = {
      cursor: SuiEventsCursor;
      hasNextPage: boolean;
    };
    
    const executeEventJob = async (
      client: SuiClient,
      tracker: EventTracker,
      cursor: SuiEventsCursor
    ): Promise<EventExecutionResult> => {
      try {
        // get the events from the chain.
        // For this implementation, we are going from start to finish.
        // This will also allow filling in a database from scratch!
        const { data, hasNextPage, nextCursor } = await client.queryEvents({
          query: tracker.filter,
          cursor,
          order: "ascending",
        });
    
        // handle the data transformations defined for each event
        await tracker.callback(data, tracker.type);
    
        // We only update the cursor if we fetched extra data (which means there was a change).
        if (nextCursor && data.length > 0) {
          await saveLatestCursor(tracker, nextCursor);
    
          return {
            cursor: nextCursor,
            hasNextPage,
          };
        }
      } catch (e) {
        console.error(e);
      }
      // By default, we return the same cursor as passed in.
      return {
        cursor,
        hasNextPage: false,
      };
    };
    
    /**
     * Saves the latest cursor for an event tracker to the db, so we can resume
     * from there.
     * */
    const saveLatestCursor = async (tracker: EventTracker, cursor: EventId) => {
      const data = {
        eventSeq: cursor.eventSeq,
        txDigest: cursor.txDigest,
      };
    
      return prisma.cursor.upsert({
        where: {
          id: tracker.type,
        },
        update: data,
        create: { id: tracker.type, ...data },
      });
    };

2.3 索引器执行

2.3.1 启动索引器

执行启动命令:npx ts-node indexer.ts,可以观察到以下日志,这是将我们在《SUI Move官方示例合约实践——NFT类:零信任原子交换(trustless swap)》一文中测试添加的数据订阅了下来。

image.png

2.3.2 可以使用 Prisma Studio GUI 进行查看数据

$ npx prisma studio
Prisma schema loaded from prisma/schema.prisma
Prisma Studio is up on http://localhost:5555

image.png

image.png

至此我们便完成了索引器的创建,接下来我们将以下将结合使用 Express 构建一个索引器的 RESTful API服务,方便应用系统进行访问查询链上数据。

3 API服务构建

3.1 工程创建

$ mkdir api-service
$ pnpm init
$ pnpm add typescript prisma ts-node  @types/node @types/cors @types/express -D
$ pnpm add @mysten/sui @prisma/client express cors

3.2 代码实现

3.2.1 请求查询语句where条件解析

该函数将提取URL查询字符串并将其解析为Prisma的有效查询参数。

export enum WhereParamTypes {
  STRING,
  NUMBER,
  BOOLEAN,
}

export type WhereParam = {
  key: string;
  type: WhereParamTypes;
};

/** Parses a where statement based on the query params. */
export const parseWhereStatement = (
  query: Record<string, any>,
  acceptedParams: WhereParam[]
) => {
  const params: Record<string, any> = {};
  for (const key of Object.keys(query)) {
    const whereParam = acceptedParams.find((x) => x.key === key);
    if (!whereParam) continue;

    const value = query[key];
    if (whereParam.type === WhereParamTypes.STRING) {
      params[key] = value;
    }
    if (whereParam.type === WhereParamTypes.NUMBER) {
      const number = Number(value);
      if (isNaN(number)) throw new Error(`Invalid number for ${key}`);

      params[key] = number;
    }

    // Handle boolean expected values.
    if (whereParam.type === WhereParamTypes.BOOLEAN) {
      let boolValue;
      if (value === "true") boolValue = true;
      else if (value === "false") boolValue = false;
      else throw new Error(`Invalid boolean for ${key}`);

      params[key] = boolValue;
    }
  }
  return params;
};

3.2.2 请求查询语句分页参数解析

export type ApiPagination = {
  take?: number;
  orderBy: {
    id: "asc" | "desc";
  };
  cursor?: {
    id: number;
  };
  skip?: number;
};

/**
 * A helper to prepare pagination based on `req.query`.
 * We are doing only primary key cursor + ordering for this example.
 */
export const parsePaginationForQuery = (body: Record<string, any>) => {
  const pagination: ApiPagination = {
    orderBy: {
      id:
        Object.hasOwn(body, "sort") && ["asc", "desc"].includes(body.sort)
          ? body.sort
          : "desc",
    },
  };

  // Prepare pagination limit (how many items to return)
  if (Object.hasOwn(body, "limit")) {
    const requestLimit = Number(body.limit);

    if (isNaN(requestLimit)) throw new Error("Invalid limit value");

    pagination.take =
      requestLimit > CONFIG.DEFAULT_LIMIT ? CONFIG.DEFAULT_LIMIT : requestLimit;
  } else {
    pagination.take = CONFIG.DEFAULT_LIMIT;
  }

  // Prepare cursor pagination (which page to return)
  if (Object.hasOwn(body, "cursor")) {
    const cursor = Number(body.cursor);
    if (isNaN(cursor)) throw new Error("Invalid cursor");
    pagination.skip = 1;
    pagination.cursor = {
      id: cursor,
    };
  }

  return pagination;
};

3.2.3 API查询服务端点实现

  • 查询locked对象
const app = express();
app.use(cors());

app.use(express.json());

app.get("/", async (req, res) => {
  return res.send({ message: "🚀 API is functional 🚀" });
});

app.get("/locked", async (req, res) => {
  const acceptedQueries: WhereParam[] = [
    {
      key: "deleted",
      type: WhereParamTypes.BOOLEAN,
    },
    {
      key: "creator",
      type: WhereParamTypes.STRING,
    },
    {
      key: "keyId",
      type: WhereParamTypes.STRING,
    },
    {
      key: "objectId",
      type: WhereParamTypes.STRING,
    },
  ];

  try {
    const locked = await prisma.locked.findMany({
      where: parseWhereStatement(req.query, acceptedQueries)!,
      ...parsePaginationForQuery(req.query),
    });

    return res.send(formatPaginatedResponse(locked));
  } catch (e) {
    console.error(e);
    return res.status(400).send(e);
  }
});
  • 查询共享托管对象
app.get("/escrows", async (req, res) => {
  const acceptedQueries: WhereParam[] = [
    {
      key: "cancelled",
      type: WhereParamTypes.BOOLEAN,
    },
    {
      key: "swapped",
      type: WhereParamTypes.BOOLEAN,
    },
    {
      key: "recipient",
      type: WhereParamTypes.STRING,
    },
    {
      key: "sender",
      type: WhereParamTypes.STRING,
    },
  ];

  try {
    const escrows = await prisma.escrow.findMany({
      where: parseWhereStatement(req.query, acceptedQueries)!,
      ...parsePaginationForQuery(req.query),
    });

    return res.send(formatPaginatedResponse(escrows));
  } catch (e) {
    console.error(e);
    return res.status(400).send(e);
  }
});

app.listen(3000, () =>
  console.log(`🚀 Server ready at: http://localhost:3000`)
);

3.3 启动API服务

3.3.1 执行启动命令

$ npx ts-node server.ts 
🚀 Server ready at: http://localhost:3000
  • 访问响应
$ curl -s http://localhost:3000 | jq
{
  "message": "🚀 API is functional 🚀"
}

3.3.2 查询locked对象

(1)查询全量数据
$ curl -s http://localhost:3000/locked | jq
{
  "data": [
    {
      "id": 5,
      "objectId": "0x226c677dfc18472b5b35430d5cd1adb7036b365bb0c5c1576750d39c405fca37",
      "keyId": "0x2db3ad1a48784e5bde665aa59c2991b269204f220dfbe9bc42afb3137abb1649",
      "creator": "0xae6c01c35619d83192f82704aa3ab9b05954acb898636c6e3c1d53ce7f0265e5",
      "itemId": "0x1a0890c4a6a973f5f3fe612c7ac4574190547c80ecdddbc0430c76e927c2d270",
      "deleted": true
    },
    {
      "id": 4,
      "objectId": "0x2be9b0606ab27d387c72887d8f0a8c3f06fb91082a4e31ab20e0e2d7c82d12f3",
      "keyId": "0x0f5422ffa583ce9a2e29d9a9c2fd653996a84cf28acf8a90c9a21c1eb364f11b",
      "creator": "0xae6c01c35619d83192f82704aa3ab9b05954acb898636c6e3c1d53ce7f0265e5",
      "itemId": "0x0cf1370cc575da5cd271930683b32c14a284cf2a0d6bef57c32367eaa08dd605",
      "deleted": false
    },
......
(2)条件查询
$ curl -s http://localhost:3000/locked?objectId=0xb3a15c074e8911c1318dcc0bf9bd957bfe30b191c7578ca457b529ccfd4718c4 | jq
{
  "data": [
    {
      "id": 1,
      "objectId": "0xb3a15c074e8911c1318dcc0bf9bd957bfe30b191c7578ca457b529ccfd4718c4",
      "keyId": "0x5476982bcd1a83126d03f7b313924f1943abad14472c707dda48499c8132ee49",
      "creator": "0x956e69bc6b23b593bce92694f990e7a4306ee53543f69e4a1eaa716f4609ec08",
      "itemId": "0x2a835cae60a341b2837d0ad36affc501d670018dfccd1ba09e7aff7bc106cfde",
      "deleted": false
    }
  ],
  "cursor": 1
}

$ curl -s "http://localhost:3000/locked?keyId=0x2db3ad1a48784e5bde665aa59c2991b269204f220dfbe9bc42afb3137abb1649&deleted=true" | jq
{
  "data": [
    {
      "id": 5,
      "objectId": "0x226c677dfc18472b5b35430d5cd1adb7036b365bb0c5c1576750d39c405fca37",
      "keyId": "0x2db3ad1a48784e5bde665aa59c2991b269204f220dfbe9bc42afb3137abb1649",
      "creator": "0xae6c01c35619d83192f82704aa3ab9b05954acb898636c6e3c1d53ce7f0265e5",
      "itemId": "0x1a0890c4a6a973f5f3fe612c7ac4574190547c80ecdddbc0430c76e927c2d270",
      "deleted": true
    }
  ],
  "cursor": 5
}
(3)分页查询
$ curl -s "http://localhost:3000/locked?sort=asc&limit=2&cursor=3" | jq
{
  "data": [
    {
      "id": 4,
      "objectId": "0x2be9b0606ab27d387c72887d8f0a8c3f06fb91082a4e31ab20e0e2d7c82d12f3",
      "keyId": "0x0f5422ffa583ce9a2e29d9a9c2fd653996a84cf28acf8a90c9a21c1eb364f11b",
      "creator": "0xae6c01c35619d83192f82704aa3ab9b05954acb898636c6e3c1d53ce7f0265e5",
      "itemId": "0x0cf1370cc575da5cd271930683b32c14a284cf2a0d6bef57c32367eaa08dd605",
      "deleted": false
    },
    {
      "id": 5,
      "objectId": "0x226c677dfc18472b5b35430d5cd1adb7036b365bb0c5c1576750d39c405fca37",
      "keyId": "0x2db3ad1a48784e5bde665aa59c2991b269204f220dfbe9bc42afb3137abb1649",
      "creator": "0xae6c01c35619d83192f82704aa3ab9b05954acb898636c6e3c1d53ce7f0265e5",
      "itemId": "0x1a0890c4a6a973f5f3fe612c7ac4574190547c80ecdddbc0430c76e927c2d270",
      "deleted": true
    }
  ],
  "cursor": 5
}

$ curl -s "http://localhost:3000/locked?sort=asc&limit=2&cursor=4" | jq
{
  "data": [
    {
      "id": 5,
      "objectId": "0x226c677dfc18472b5b35430d5cd1adb7036b365bb0c5c1576750d39c405fca37",
      "keyId": "0x2db3ad1a48784e5bde665aa59c2991b269204f220dfbe9bc42afb3137abb1649",
      "creator": "0xae6c01c35619d83192f82704aa3ab9b05954acb898636c6e3c1d53ce7f0265e5",
      "itemId": "0x1a0890c4a6a973f5f3fe612c7ac4574190547c80ecdddbc0430c76e927c2d270",
      "deleted": true
    }
  ],
  "cursor": 5
}

3.3.3 查询共享托管对象

(1)查询全量数据
$ curl -s http://localhost:3000/escrows | jq
{
  "data": [
    {
      "id": 2,
      "objectId": "0xd829028ba9c7d28aa579d60402b02f346734f2534e56873a71e713fd2e5f715f",
      "sender": "0xf2e6ffef7d0543e258d4c47a53d6fa9872de4630cc186950accbd83415b009f0",
      "recipient": "0x2d178b9704706393d2630fe6cf9415c2c50b181e9e3c7a977237bb2929f82d19",
      "keyId": "0xb524ee7c3464460d7b247423bcb0ed762986fe5d1d88647c3dceccbeec2e71a5",
      "itemId": "0x4062011a41b15825b32645f7c118e435d9a97e1291bc3b6f126760bf16f021fb",
      "swapped": true,
      "cancelled": false
    },
    {
      "id": 1,
      "objectId": "0x5b24e3b1423b5ef665ac236ed44282acb0f25f914a6a37fe782dad2af7855d58",
      "sender": "0xae6c01c35619d83192f82704aa3ab9b05954acb898636c6e3c1d53ce7f0265e5",
      "recipient": "0x956e69bc6b23b593bce92694f990e7a4306ee53543f69e4a1eaa716f4609ec08",
      "keyId": "0x5476982bcd1a83126d03f7b313924f1943abad14472c707dda48499c8132ee49",
      "itemId": "0x1a0890c4a6a973f5f3fe612c7ac4574190547c80ecdddbc0430c76e927c2d270",
      "swapped": false,
      "cancelled": false
    }
  ],
  "cursor": 1
}
(2)条件查询
$ curl -s "http://localhost:3000/escrows?sender=0xae6c01c35619d83192f82704aa3ab9b05954acb898636c6e3c1d53ce7f0265e5&recipient=0x956e69bc6b23b593bce92694f990e7a4306ee53543f69e4a1eaa716f4609ec08" | jq
{
  "data": [
    {
      "id": 1,
      "objectId": "0x5b24e3b1423b5ef665ac236ed44282acb0f25f914a6a37fe782dad2af7855d58",
      "sender": "0xae6c01c35619d83192f82704aa3ab9b05954acb898636c6e3c1d53ce7f0265e5",
      "recipient": "0x956e69bc6b23b593bce92694f990e7a4306ee53543f69e4a1eaa716f4609ec08",
      "keyId": "0x5476982bcd1a83126d03f7b313924f1943abad14472c707dda48499c8132ee49",
      "itemId": "0x1a0890c4a6a973f5f3fe612c7ac4574190547c80ecdddbc0430c76e927c2d270",
      "swapped": false,
      "cancelled": false
    }
  ],
  "cursor": 1
}
(3)分页查询
$ curl -s "http://localhost:3000/escrows?sort=asc&limit=1&cursor=1" | jq
{
  "data": [
    {
      "id": 2,
      "objectId": "0xd829028ba9c7d28aa579d60402b02f346734f2534e56873a71e713fd2e5f715f",
      "sender": "0xf2e6ffef7d0543e258d4c47a53d6fa9872de4630cc186950accbd83415b009f0",
      "recipient": "0x2d178b9704706393d2630fe6cf9415c2c50b181e9e3c7a977237bb2929f82d19",
      "keyId": "0xb524ee7c3464460d7b247423bcb0ed762986fe5d1d88647c3dceccbeec2e71a5",
      "itemId": "0x4062011a41b15825b32645f7c118e435d9a97e1291bc3b6f126760bf16f021fb",
      "swapped": true,
      "cancelled": false
    }
  ],
  "cursor": 2
}

4 参考资料

https://docs.sui.io/guides/developer/app-examples/trustless-swap/indexer-api

5 更多

欢迎关注微信公众号:Move中文,开启你的 Sui Move 之旅!

image.png

点赞 0
收藏 1
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
rzexin
rzexin
0x6Fa5...8165
江湖只有他的大名,没有他的介绍。