Solana web3 - 条件查询

1,719 阅读3分钟

使用web3查询,做nft的话最常用的是getProgramAccounts函数,可是很多数据,我们只需要一部分,怎么办?

像官方的metaplex代码,写的很烂,数据都是全网抓取,不适合商用。那如果我们需要查询加灵活的条件怎么办呢,这时候web的利器memcmp来了,可以根据字节的位置查询数据。

在我上一个文章有一些基本account数据的获取方式 Solana DApp开发之 获取各种account数据的方式

但是有些数据,和account绑定关系不是那么纯粹的时候,我们就需要使用复杂查询了(主要是accountInfo函数数据实在有限)

关于条件查询

myBidderPot例子

先使用BidderPotParser 解码;

SCHEME

[
    BidderPot,
    {
      kind: 'struct',
      fields: [
        ['bidderPot', 'pubkeyAsString'],
        ['bidderAct', 'pubkeyAsString'],
        ['auctionAct', 'pubkeyAsString'],
        ['emptied', 'u8'],
      ],
    },
]

filter例子

filters: [
    {
        dataSize: BIDDER_POT_LEN,
    },
    {
        memcmp: {
            offset: 0,
            bytes: '7725SAfE76PUNuXReZZvn8xSBEn1K65bzgnCAvNuEUP3',
        },
    },
],

dataSize就是占用的空间大小

memcmp就是数据内存匹配,offset就是起点,不需要终点,终点按bytes长度加offset算的。bytes不需要编码,因为编码数据是无法匹配的(片段数据的编码和完整数据编码,是完全对不上的)。

offset的话,从0开始。

其他的字段开始根据SCHEME计算。比如查询auctionAct信息,offset就是32 + 32,也就是64.(不需要-1)。

多个条件查询完整例子

import { Connection } from '@solana/web3.js';
import { getProgramAccounts } from 'contexts/meta/loadAccounts';
import { AUCTION_ID, BIDDER_POT_LEN } from 'npms/oystoer';

interface myBidderPotProps {
  connection: Connection;
  walletPubkey: string;
  auctionPubkey: string;
}

export const getMyBidderPot = async ({
  connection,
  walletPubkey,
  auctionPubkey,
}: myBidderPotProps) => {
  const biderPot = await getProgramAccounts(connection, AUCTION_ID, {
    filters: [
      {
        dataSize: BIDDER_POT_LEN,
      },
      {
        memcmp: {
          offset: 32,
          //  bidderAct
          bytes: walletPubkey,
        },
      },
      {
        memcmp: {
          offset: 32 + 32,
          //  auctionAct
          bytes: auctionPubkey,
        },
      },
    ],
  });
  return biderPot;
};

根据account查询Edition

像Solana web3里面accountInfo方法masteredtion地址是可以查询出来的(使用json,base64不行),但是edtion就不可以这样查询了

现有开源代码里面 metaplex是这样做的

// 使用getProgramAccounts,根据METADATA_PROGRAM_ID数据数据
getProgramAccounts(connection, METADATA_PROGRAM_ID).then(
  forEach(processMetaData),
)
// 遍历后,前端过滤buffer数据
const isEditionV1Account = (account: AccountInfo<Buffer>) =>
  account.data[0] === MetadataKey.EditionV1;

// 这是edtion数据格式
export class Edition {
  key: MetadataKey;
  /// Points at MasterEdition struct
  parent: StringPublicKey;
  /// Starting at 0 for master record, this is incremented for each edition minted.
  edition: BN;
  constructor(args: {
    key: MetadataKey;
    parent: StringPublicKey;
    edition: BN;
  }) {
    this.key = MetadataKey.EditionV1;
    this.parent = args.parent;
    this.edition = args.edition;
  }
}
// 这是SCHEME格式
[
    Edition,
    {
      kind: 'struct',
      fields: [
        ['key', 'u8'],
        ['parent', 'pubkeyAsString'],
        ['edition', 'u64'],
      ],
    }
]

看这样子,单查询的话,我们需要根据 parent来查询,offset就是u8的大小,也就是1.

这样filter就可以这样写

filters: [
  {
    memcmp: {
      offset: 1,
      bytes: parentPubkey,
    },
  },
]

这样我们可以获取edtion列表了,但是,未必这就代表他是edtion数据,因为少了头部字节的判断,但是这个判断,和上面的不太一样,这个怎么整?

有两种,1是前端过滤,这是最简单的,因为数据被过滤后本身不多了,所以问题不大;2是直接由提供节点的web3处理,我们说下这个情况。

这里我们先理解下,下面这个Buffer其实是base64或者bse58解码后的数据,也就是可以和查询直接对上关系,所以account.data[0]就代表了schema上面的第一个字段里面的内容,这个值是有范围的,u8刚好。我们要查询edtion,看下schema,这个其实就是key值啦,所以直接加一个条件offset: 0,即可。 这时候filters代码如下:

const isEditionV1Account = (account: AccountInfo<Buffer>) =>
  account.data[0] === MetadataKey.EditionV1;
filters: [
  {
    memcmp: {
      offset: 0,
      bytes: bytes: base58.encode(new Uint8Array([MetadataKey.EditionV1])),
    },
  }
  {
    memcmp: {
      offset: 1,
      bytes: parentPubkey,
    },
  },
]

export enum MetadataKey {
  Uninitialized = 0,
  MetadataV1 = 4,
  EditionV1 = 1,
  MasterEditionV1 = 2,
  MasterEditionV2 = 6,
  EditionMarker = 7,
}

完整函数代码

export const accountGetEditionInfo = async ({
  parentPubkey,
  connection,
}: {
  parentPubkey: string;
  connection: Connection;
}) => {
  const res = await getProgramAccounts(connection, METADATA_PROGRAM_ID, {
    filters: [
      {
        memcmp: {
          offset: 0,
          bytes: base58.encode(new Uint8Array([MetadataKey.EditionV1])),
        },
      },
      {
        memcmp: {
          offset: 1,
          bytes: parentPubkey,
        },
      },
    ],
  });
  return res;
};

注意

bs58.encode(Buffer.from(key)) ×

base58.encode(new Uint8Array([key]))

Int类型不能用Buffer

内存大小

基础类型占用空间大小表

类型内存大小
pubkeyAsString32
u648
u81

鸣谢

  • 感谢Solana群友fun的热心帮助
  • 关于key类型(数字类型)的过滤,感谢discord Solana社区的chido指证

--完--