流式数据获取—基于Opensea Stream Api的Golang实践
笔者为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进行开发尝试。
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社区
相关推荐
一个Javascript SDK,用于从OpenSea Stream API接收更新 - 通过websockets推送
一个Javascript SDK,用于从OpenSea Stream API接收更新 - 通过websockets推送
高校及科研院所如何利用外部数据完善技术转移转化机制.docx
科易网基于40亿+科创知识图谱数据库,深度探索AI技术在技术转移、成果转化、技术经纪、知识产权、产业创新、科技招商等垂直领域的多样化应用场景,研究科技创新领域的AI+数智化解决方案,推动科技创新与产业创新智能化发展。
完全复现虚拟同步发电机VSG自适应惯量阻尼控制仿真、根轨迹、调节系数对比(Simulink仿真、Matlab代码实现)
内容概要:本文档围绕“【完全复现】虚拟同步发电机VSG自适应惯量阻尼控制仿真、根轨迹、调节系数对比”这一主题,利用Simulink和Matlab平台,系统性地实现了虚拟同步发电机(VSG)的自适应惯量与阻尼控制策略的建模与仿真。内容涵盖VSG的核心控制原理、系统动态响应特性分析、根轨迹绘制以及在不同调节系数下的性能对比研究,旨在深入剖析关键控制参数对系统稳定性与动态性能的影响机制,为新能源并网系统中提升频率支撑能力和暂态稳定性的控制策略设计提供理论依据和技术支持。; 适合人群:具备扎实的电力系统分析与自动控制理论基础,熟练掌握Matlab/Simulink仿真工具,主要从事新能源发电、微电网运行、电力电子变换器控制或电力系统稳定研究的研究生、科研人员及工程技术人员。; 使用场景及目标:①开展虚拟同步发电机(VSG)的建模与先进控制策略的深入研究;②探究自适应惯量与阻尼控制对改善电网频率响应速度和平抑功率波动的作用机理;③运用根轨迹法进行系统稳定性分析,并指导控制器参数的优化整定;④完成高水平学术论文撰写或科研项目申报所需的仿真复现实验。; 阅读建议:本资源以“完全复现”为核心目标,提供了完整的仿真模型与配套代码。使用者应在深入理解同步发电机转子运动方程、虚拟惯量及阻尼物理意义的基础上,结合电力系统暂态稳定理论,逐步操作并调试仿真模型,尤其需关注不同调节系数变化对系统动态性能指标(如超调量、调节时间)的影响,从而深化对VSG内在控制机制与稳定边界的认知。
前端开发基于history.js的HTML5与HTML4浏览器兼容方案:单页应用无刷新历史状态管理技术实现-dQ4ehf7C1792158475
内容概要:本文系统介绍了如何利用history.js实现单页应用(SPA)在HTML5c与HTML4浏览器间s草错发的无缝历史状态管理。通过封装原生History API并在不支持的浏览器中自动降级为哈希路由,history.js解决了因浏览器兼容性导致的后退按钮失效、URL混乱等问题。文章详细讲解了其核心机制,包括状态对象管理、自动模式切换(HTML5 pushState vs HTML4 哈希)、状态数据持久化以及针,对Safari、IE等浏\览器的兼容性修复方案,并提供了从入门到实战的完整示例,涵盖初始化、事,件监听、页面加载及性能优化技巧。;DtPXB1792158476
员工满意度数据集(11列,12000 条记录)CSV 合成
详情介绍:该 HR 员工数据集含 12,000 条 ×11 列(满意度、绩效评估、项目数、月均工时、司龄、工伤、福利包、晋升、部门、薪资),无缺失值但无离职标签列,且存在数据质量问题(141 条负绩效评估、144 条负工时、49.4% 异常高工伤率、项目数仅 0/1/2),疑似经过修改或合成,不能直接做经典的员工离职预测,适合做满意度回归、薪资三分类、晋升预测或员工聚类分析,使用前必须清洗异常值并注意数据真实性限制。


