46
社区成员




笔者为Golang小白,边学边开发,若代码不elegant,还请多多见谅!同时非常乐意接受批评建议。
本文围绕OpenSea的Websocket流式数据接口api:Stream API Overview (Beta) (opensea.io),官方实现是基于JavaScript。Go语言作为区块链领域的热门开发工具,广受大家青睐,程序简洁易于编译部署,本文尝试借鉴相关资料:GitHub - foundVanting/opensea-stream-go: listen opensea Stream API with golang,基于Golang实现该接口的流式数据获取并存储于Clickhouse和Hbase2.x版本中(注:本文仅以上架事件Item_List作为开发实例)。
接口介绍可以参看官方文档Stream API Overview (Beta) (opensea.io),但是其文档写得确实有点拉胯(关键信息全靠自己掂量)。结合文档及自己的理解,简要梳理了一下:
七大事件
item listed:主要包含的是NFT拥有者自己所出的卖出报价(上架信息),价格自己叫;
item sold:主要包含的是NFT的售卖信息;
item transferred:主要包含的是NFT的转移信息,包括Mint过程,买卖过程,转移赠送过程等;
item metadata updates:主要包含的是NFT的元数据信息的变更;
item cancelled:主要包含的是NFT拥有者对自己之前的上架信息进行取消,即取消报价;
item received offer:主要包含的是其他人对买入该NFT所出的价格;
item received bid:我理解的是类似于item received offer事件,包含的是其他人对买入该NFT所出的价格;因为bid/Offer事件同属于OpenSea的Offers模块,只不过两个事件中的maker和taker字段存在着不同。bid和offer都是买单。通过检查订单对象上的taker来区分bid和offer。taker地址为空的订单,taker是offer。taker地址不是空地址的订单,taker是bid方。
对于item listed上架事件来说,其中最主要的就是价格和过期时间字段的数据,因此在获取数据时,需获取的是有效时间内的报价信息。而且用于有效期内的报价信息存在多个的情况,因此在保存数据到Hbase的过程中,需要存储的是一个报价队列(切片),将有效期内的所有报价数据全部存储下来。
图1 Item_listed事件对应关系图
代码程序文件:opensea_stream_api.go.
我这里主要借鉴的是Go语言中常用的生产者—消费者模式进行流式数据的处理方式。生产者模式用于获取api中的数据并起到一定的缓存作用,消费者模式用于处理通道Channel中的数据,从而使得生产-消费过程解耦。笔者第一次体验,确实感觉Channel挺香的。
生产者模式
图2 生产者模式
生产者模式下的OnItemListed函数中的“*”代表获取的是全部NFT Collection中的所有相关NFT的上架信息,当然,若想获取单个的Collection下的NFT的上架信息,仅需将“*”替换成对应的Collection的slug名称即可。
消费者模式
图3 消费者模式
此处由于代码逻辑处理较复杂,因此仅放了核心的步骤,通过for循环遍历通道中的数据,通过select语句会监听和Channel有关的IO操作,能有效防止了通道Channel阻塞情况(笔者一开始没有注意用select,导致print正常,但是一直无法插入数据到相应的数据库)。
完整示例:
代码结构树
图4 代码结构树
其中go-hbase的相关内容可见巨佬文章:
如何用Go语言快速方便操作HBase2.0.x —— 基于github.com/pingcap/go-hbase的Hack实践-CSDN社区
用Go语言操作HBase2.x实现列查询结果过滤——基于github.com/pingcap/go-hbase的魔改实践-CSDN社区
数据样例及schema:
{
BaseStreamMessage: {
EventType: item_listed SentAt: 2022 - 06 - 22 T04: 27: 30.548418 + 00: 00
}
Payload: {
PayloadItemAndColl: {
Item: {
NftId: matic / 0x2953399124f0cbb46d2cbacd8a89cf0599974963 / 109948715392367378689889311402377798690559221593450944300256613354821897945089 Permalink: https: //opensea.io/assets/matic/0x2953399124f0cbb46d2cbacd8a89cf0599974963/109948715392367378689889311402377798690559221593450944300256613354821897945089 Metadata:{Name:Fancy Art NFT #3466 ImageUrl:https://lh3.googleusercontent.com/70My0zp7KimqRPO3kNi17xhQUhkVtQ5d236sYcp4a4g5-IUDNhHrGM3mtqnk2QBc_8_RswgvMtdMZwju2ixySnx1LU56-uR4wRgmZA=s250 AnimationUrl: MetadataUrl:} Chain:{Name:matic}} Collection:{Slug:fancy-by-art-ai}} Quantity:1 ListingType: ListingDate:2022-06-22T04:26:52.000000+00:00 ExpirationDate:2022-11-08T05:26:52.000000+00:00 Maker:{Address:0xf314c481ef7cd3202026134c98a2c312b13423bd} Taker:{Address:} BasePrice:125000000000000000 PaymentToken:{Address:0x7ceb23fd6bc0add59e62ac25578270cff1b9f619 Decimals:18
EthPrice: 1.000000000000000 Name: Ether Symbol: ETH UsdPrice: 1129.099999999999909000
}
IsPrivate: false EventTimestamp: 2022 - 06 - 22 T04: 27: 30.521529 + 00: 00
}
}
官网提供的数据Schema压根对不上真实的数据。
图5 Opensea Item_listed事件数据schema
package main
import (
"database/sql"
"encoding/json"
"fmt"
"log"
"sort"
"strconv"
"strings"
"time"
_ "github.com/ClickHouse/clickhouse-go"
"github.com/ethereum/go-ethereum/crypto"
"github.com/foundVanting/opensea-stream-go/entity"
"github.com/foundVanting/opensea-stream-go/opensea"
"github.com/foundVanting/opensea-stream-go/types"
_ "github.com/go-sql-driver/mysql"
"github.com/mitchellh/mapstructure"
"github.com/nshafer/phx"
)
func main() {
// load configuration
envConf := Config{}
dbAggr := DbAggr{}
ReadConfig("./config.yml", &envConf)
fmt.Printf("Start opensea stream api event subscription ...\n")
SetupDbClients(&dbAggr, envConf.Database)
//define channel
ch_list := make(chan entity.ItemListedEvent, 128)
go Producer_Events(ch_list) // data producer
dbAggr.Consumer_Events(ch_list) // data consumer
}
// Configs
const CH_conf = "tcp://***.***.***.***:9000?username=*****&password=****&database=test&debug=true"
const Hbase_table = "nft:mktevent"
const APi_key = "*****************************"
const zone = "Asia/Shanghai"
type EventList struct {
EventType string `json:"eventType"`
Maker string `json:"maker"`
Taker string `json:"taker"`
TradeMarket string `json:"tradeMarket"`
ExprTime string `json:"expirationTs"`
EventTime string `json:"eventTs"`
Currency string `json:"currency"`
Price string `json:"price"`
}
const sql_list = "INSERT INTO nft_mktevent(contrAddr,tokenId,chainId,slug,eventType,expirationTs,maker,taker,price,currency, txMkt, eventTs) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
// Producer Model
func Producer_Events(out_list chan<- entity.ItemListedEvent) {
client := opensea.NewStreamClient(types.MAINNET, APi_key, phx.LogInfo, func(err error) {
fmt.Println("opensea.NewStreamClient err:", err)
})
client.Connect()
// ItemList Event
client.OnItemListed("*", func(response any) {
var itemListedEvent entity.ItemListedEvent
err := mapstructure.Decode(response, &itemListedEvent)
if err != nil {
fmt.Println("mapstructure.Decode err:", err)
}
out_list <- itemListedEvent
})
}
// Consumer Model
func (db *DbAggr) Consumer_Events(read_list <-chan entity.ItemListedEvent) {
conn, err := sql.Open("clickhouse", CH_conf) // connect to CH
if err != nil {
log.Fatal(err)
}
/*
ItemList Event
*/
for {
select {
case item := <-read_list:
tx, err := conn.Begin()
if err != nil {
log.Fatal(err)
}
stmt, err := tx.Prepare(sql_list)
if err != nil {
log.Fatal(err)
}
defer stmt.Close()
nftid := item.Payload.PayloadItemAndColl.Item.NftId
addr_cont_id := strings.Split(nftid, "/")
chain, addr, tokenid := addr_cont_id[0], strings.Replace(addr_cont_id[1], "0x", "", -1), addr_cont_id[2]
chain_id := chain_trans(chain)
slug := item.Payload.PayloadItemAndColl.Collection.Slug
event_type := item.BaseStreamMessage.EventType
expire_dt := item.Payload.ExpirationDate
if len(expire_dt) >= 19 {
expire_dt = strings.Replace(expire_dt[:19], "T", " ", -1)
} else {
fmt.Printf("this time is: %v\n", expire_dt)
}
lz, _ := time.LoadLocation(zone)
t1, _ := time.ParseInLocation("2006-01-02 15:04:05", expire_dt, lz)
tmStr1 := fmt.Sprintf("%s", time.Unix(t1.Unix(), 0).UTC())
tmVals1 := strings.Split(tmStr1, " ")
tmRes1 := strings.Join(tmVals1[0:2], " ")
maker := strings.Replace(item.Payload.Maker.Address, "0x", "", -1)
taker := strings.Replace(item.Payload.Taker.Address, "0x", "", -1)
price := item.Payload.BasePrice // string
symbol := item.Payload.PaymentToken.Symbol
currency := currency_trans(symbol)
event_dt := item.Payload.EventTimestamp
if len(event_dt) >= 19 {
event_dt = strings.Replace(event_dt[:19], "T", " ", -1)
} else {
fmt.Printf("this time is: %v\n", event_dt)
}
t2, _ := time.ParseInLocation("2006-01-02 15:04:05", event_dt, lz)
tmStr2 := fmt.Sprintf("%s", time.Unix(t2.Unix(), 0).UTC())
tmVals2 := strings.Split(tmStr2, " ")
tmRes2 := strings.Join(tmVals2[0:2], " ")
mkt := uint16(1) // opensea: 1
// contrAddr,tokenId,chainId,slug,eventType,expirationTs,maker,taker,price,currency, txMkt, eventTs
if _, err := stmt.Exec(addr, tokenid, chain_id, slug, event_type, tmRes1, maker, taker, price, currency, mkt, tmRes2); err != nil {
log.Printf("Error in executing clickhouse insert: %v", err)
}
if err := tx.Commit(); err != nil {
log.Printf("Error in committing clickhouse insert: %v", err)
}
// HBase
chainHex := fmt.Sprintf("%064x", chain_id)
rowKeyToken := fmt.Sprintf("%064x", crypto.Keccak256([]byte(addr+tokenid+chainHex)))
expire_ts := t1.UnixNano() / 1e6
event_ts := t2.UnixNano() / 1e6
// generate event data slice
List := []EventList{
EventList{
EventType: event_type,
Maker: maker,
Taker: taker,
TradeMarket: fmt.Sprintf("%d", mkt),
ExprTime: fmt.Sprintf("%d", expire_ts),
EventTime: fmt.Sprintf("%d", event_ts),
Currency: fmt.Sprintf("%d", currency),
Price: price,
},
}
list_obj, _ := json.Marshal(List) // serialize
values := map[string]map[string][]byte{
"info": {
"contract": []byte(addr),
"tokenid": []byte(tokenid),
"chainid": []byte(fmt.Sprintf("%d", chain_id)),
},
"quote": {
"event_List": list_obj,
},
}
// get rowkey's list event data
fmCols := map[string][]string{"info": {"contract", "tokenid", "chainid"}, "quote": {"event_List"}}
res, _ := db.hbClient.GetByRowKey(Hbase_table, rowKeyToken, fmCols)
if res != nil {
var data_event []EventList
exist_list := res.Columns["quote:event_List"] // columns exist or not
if exist_list != nil {
error2 := json.Unmarshal(res.Columns["quote:event_List"].Value, &data_event)
if error2 != nil {
log.Printf("Error while getting hbase row: table-%s, row-%s", Hbase_table, rowKeyToken)
}
// define a slice
var item_event []EventList
now := time.Now()
now_ts := now.UnixNano() / 1e6 // now timestamp
// add the new event data first
data_event = append(data_event, List[0])
data_len := len(data_event)
if data_len > 1 {
for idx := 0; idx < data_len-1; idx++ {
intNum, _ := strconv.Atoi(data_event[idx].ExprTime)
if int64(intNum) >= now_ts && data_event[idx] != data_event[idx+1] {
item_event = append(item_event, data_event[idx])
}
// process the last data
if idx == len(item_event)-2 {
if int64(intNum) >= now_ts {
item_event = append(item_event, item_event[idx+1])
}
}
}
} else if data_len == 1 {
for _, item := range data_event {
intNum, _ := strconv.Atoi(item.ExprTime)
if int64(intNum) >= now_ts {
item_event = append(item_event, item) // filter
}
}
} else {
fmt.Println("null data for list search.")
}
// sort
if len(item_event) > 1 {
sort.Slice(item_event, func(i, j int) bool {
return item_event[i].Price < item_event[j].Price // List price ascending
})
}
// put update data
dd, _ := json.Marshal(item_event)
values1 := map[string]map[string][]byte{
"info": {
"contract": []byte(addr),
"tokenid": []byte(tokenid),
"chainid": []byte(fmt.Sprintf("%d", chain_id)),
},
"quote": {
"event_List": dd,
},
}
db.hbClient.PutByRowKey(Hbase_table, rowKeyToken, values1)
}
} else {
db.hbClient.PutByRowKey(Hbase_table, rowKeyToken, values)
}
default:
continue
}
}
}
func chain_trans(chain string) uint16 {
if chain == "ethereum" {
return 1
} else if chain == "matic" {
return 2
} else if chain == "Klaytn" {
return 3
} else if chain == "solana" {
return 4
} else {
return 0
}
}
func currency_trans(symbol string) uint16 {
if symbol == "ETH" {
return 1
} else if symbol == "WETH" {
return 2
} else if symbol == "BTC" {
return 3
} else {
return 0
}
}
示例仅作为参考,仅作为一个思路和开发流程参考,里面涉及到的代码树中诸多文件,作为内部资料,暂时没有公开。APi_key可在Request an API key (opensea.io)申请。
本文基于Go语言实现了OpenSea的Websocket流式api的数据获取过程,同时借鉴优雅的Channel设计进行生产-消费者模式的开发,使得程序更简炼,可读性更强。对于类似的数据场景,推荐学习和使用Golang进行开发尝试。
Stream API Overview (Beta) (opensea.io)
GitHub - foundVanting/opensea-stream-go: listen opensea Stream API with golang
如何用Go语言快速方便操作HBase2.0.x —— 基于github.com/pingcap/go-hbase的Hack实践-CSDN社区
用Go语言操作HBase2.x实现列查询结果过滤——基于github.com/pingcap/go-hbase的魔改实践-CSDN社区