流式数据获取—基于Opensea Stream Api的Golang实践

小小叮当同学 2022-07-09 22:42:55

笔者为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实现该接口的流式数据获取并存储于ClickhouseHbase2.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进行开发尝试。

     

     

    reference

      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社区

...全文
1010 回复 打赏 收藏 转发到动态 举报
AI 作业
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

46

社区成员

发帖
与我相关
我的任务
社区描述
这里是CSDN讨论Web产品技术和产业发展的大本营, 欢迎所有Web3业内和关心Web3的朋友们.
社区管理员
  • Web3天空之城
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧