shared.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. package p
  2. import (
  3. "context"
  4. "log"
  5. "math"
  6. "net/http"
  7. "os"
  8. "strings"
  9. "sync"
  10. "cloud.google.com/go/bigtable"
  11. "cloud.google.com/go/pubsub"
  12. "github.com/certusone/wormhole/node/pkg/vaa"
  13. )
  14. // shared code for the various functions, primarily response formatting.
  15. // client is a global Bigtable client, to avoid initializing a new client for
  16. // every request.
  17. var client *bigtable.Client
  18. var clientOnce sync.Once
  19. var tbl *bigtable.Table
  20. var pubsubClient *pubsub.Client
  21. var pubSubTokenTransferDetailsTopic *pubsub.Topic
  22. var coinGeckoCoins = map[string][]CoinGeckoCoin{}
  23. var solanaTokens = map[string]SolanaToken{}
  24. // init runs during cloud function initialization. So, this will only run during an
  25. // an instance's cold start.
  26. // https://cloud.google.com/functions/docs/bestpractices/networking#accessing_google_apis
  27. func init() {
  28. clientOnce.Do(func() {
  29. // Declare a separate err variable to avoid shadowing client.
  30. var err error
  31. project := os.Getenv("GCP_PROJECT")
  32. instance := os.Getenv("BIGTABLE_INSTANCE")
  33. client, err = bigtable.NewClient(context.Background(), project, instance)
  34. if err != nil {
  35. // http.Error(w, "Error initializing client", http.StatusInternalServerError)
  36. log.Printf("bigtable.NewClient error: %v", err)
  37. return
  38. }
  39. var pubsubErr error
  40. pubsubClient, pubsubErr = pubsub.NewClient(context.Background(), project)
  41. if pubsubErr != nil {
  42. log.Printf("pubsub.NewClient error: %v", pubsubErr)
  43. return
  44. }
  45. })
  46. tbl = client.Open("v2Events")
  47. // create the topic that will be published to after decoding token transfer payloads
  48. tokenTransferDetailsTopic := os.Getenv("PUBSUB_TOKEN_TRANSFER_DETAILS_TOPIC")
  49. if tokenTransferDetailsTopic != "" {
  50. pubSubTokenTransferDetailsTopic = pubsubClient.Topic(tokenTransferDetailsTopic)
  51. // fetch the token lists once at start up
  52. coinGeckoCoins = fetchCoinGeckoCoins()
  53. solanaTokens = fetchSolanaTokenList()
  54. }
  55. }
  56. var columnFamilies = []string{
  57. "MessagePublication",
  58. "QuorumState",
  59. "TokenTransferPayload",
  60. "AssetMetaPayload",
  61. "NFTTransferPayload",
  62. "TokenTransferDetails",
  63. "ChainDetails",
  64. }
  65. var messagePubFam = columnFamilies[0]
  66. var quorumStateFam = columnFamilies[1]
  67. var transferPayloadFam = columnFamilies[2]
  68. var metaPayloadFam = columnFamilies[3]
  69. var nftPayloadFam = columnFamilies[4]
  70. var transferDetailsFam = columnFamilies[5]
  71. var chainDetailsFam = columnFamilies[6]
  72. type (
  73. // Summary is MessagePublication data & QuorumState data
  74. Summary struct {
  75. EmitterChain string
  76. EmitterAddress string
  77. Sequence string
  78. InitiatingTxID string
  79. Payload []byte
  80. SignedVAABytes []byte
  81. QuorumTime string
  82. TransferDetails *TransferDetails
  83. }
  84. // Details is a Summary extended with all the post-processing ColumnFamilies
  85. Details struct {
  86. Summary
  87. SignedVAA *vaa.VAA
  88. TokenTransferPayload *TokenTransferPayload
  89. AssetMetaPayload *AssetMetaPayload
  90. NFTTransferPayload *NFTTransferPayload
  91. ChainDetails *ChainDetails
  92. }
  93. // The following structs match the ColumnFamiles they are named after
  94. TokenTransferPayload struct {
  95. Amount string
  96. OriginAddress string
  97. OriginChain string
  98. TargetAddress string
  99. TargetChain string
  100. }
  101. AssetMetaPayload struct {
  102. TokenAddress string
  103. TokenChain string
  104. Decimals string
  105. Symbol string
  106. Name string
  107. CoinGeckoCoinId string
  108. NativeAddress string
  109. }
  110. NFTTransferPayload struct {
  111. OriginAddress string
  112. OriginChain string
  113. Symbol string
  114. Name string
  115. TokenId string
  116. URI string
  117. TargetAddress string
  118. TargetChain string
  119. }
  120. TransferDetails struct {
  121. Amount string
  122. Decimals string
  123. NotionalUSDStr string
  124. TokenPriceUSDStr string
  125. TransferTimestamp string
  126. OriginSymbol string
  127. OriginName string
  128. OriginTokenAddress string
  129. // fields below exist on the row, but no need to return them currently.
  130. // NotionalUSD uint64
  131. // TokenPriceUSD uint64
  132. }
  133. ChainDetails struct {
  134. SenderAddress string
  135. ReceiverAddress string
  136. }
  137. )
  138. func chainIdStringToType(chainId string) vaa.ChainID {
  139. switch chainId {
  140. case "1":
  141. return vaa.ChainIDSolana
  142. case "2":
  143. return vaa.ChainIDEthereum
  144. case "3":
  145. return vaa.ChainIDTerra
  146. case "4":
  147. return vaa.ChainIDBSC
  148. case "5":
  149. return vaa.ChainIDPolygon
  150. }
  151. return vaa.ChainIDUnset
  152. }
  153. func makeSummary(row bigtable.Row) *Summary {
  154. summary := &Summary{}
  155. if _, ok := row[messagePubFam]; ok {
  156. for _, item := range row[messagePubFam] {
  157. switch item.Column {
  158. case "MessagePublication:InitiatingTxID":
  159. summary.InitiatingTxID = string(item.Value)
  160. case "MessagePublication:Payload":
  161. summary.Payload = item.Value
  162. case "MessagePublication:EmitterChain":
  163. summary.EmitterChain = string(item.Value)
  164. case "MessagePublication:EmitterAddress":
  165. summary.EmitterAddress = string(item.Value)
  166. case "MessagePublication:Sequence":
  167. summary.Sequence = string(item.Value)
  168. }
  169. }
  170. } else {
  171. // Some rows have a QuorumState, but no MessagePublication,
  172. // so populate Summary values from the rowKey.
  173. keyParts := strings.Split(row.Key(), ":")
  174. chainId := chainIdStringToType(keyParts[0])
  175. summary.EmitterChain = chainId.String()
  176. summary.EmitterAddress = keyParts[1]
  177. seq := strings.TrimLeft(keyParts[2], "0")
  178. if seq == "" {
  179. seq = "0"
  180. }
  181. summary.Sequence = seq
  182. }
  183. if _, ok := row[quorumStateFam]; ok {
  184. item := row[quorumStateFam][0]
  185. summary.SignedVAABytes = item.Value
  186. summary.QuorumTime = item.Timestamp.Time().String()
  187. }
  188. if _, ok := row[transferDetailsFam]; ok {
  189. transferDetails := &TransferDetails{}
  190. for _, item := range row[transferDetailsFam] {
  191. switch item.Column {
  192. case "TokenTransferDetails:Amount":
  193. transferDetails.Amount = string(item.Value)
  194. case "TokenTransferDetails:Decimals":
  195. transferDetails.Decimals = string(item.Value)
  196. case "TokenTransferDetails:NotionalUSDStr":
  197. transferDetails.NotionalUSDStr = string(item.Value)
  198. case "TokenTransferDetails:TokenPriceUSDStr":
  199. transferDetails.TokenPriceUSDStr = string(item.Value)
  200. case "TokenTransferDetails:TransferTimestamp":
  201. transferDetails.TransferTimestamp = string(item.Value)
  202. case "TokenTransferDetails:OriginSymbol":
  203. transferDetails.OriginSymbol = string(item.Value)
  204. case "TokenTransferDetails:OriginName":
  205. transferDetails.OriginName = string(item.Value)
  206. case "TokenTransferDetails:OriginTokenAddress":
  207. transferDetails.OriginTokenAddress = string(item.Value)
  208. }
  209. }
  210. summary.TransferDetails = transferDetails
  211. }
  212. return summary
  213. }
  214. func makeDetails(row bigtable.Row) *Details {
  215. deets := &Details{}
  216. sum := makeSummary(row)
  217. deets.Summary = Summary{
  218. EmitterChain: sum.EmitterChain,
  219. EmitterAddress: sum.EmitterAddress,
  220. Sequence: sum.Sequence,
  221. InitiatingTxID: sum.InitiatingTxID,
  222. Payload: sum.Payload,
  223. SignedVAABytes: sum.SignedVAABytes,
  224. QuorumTime: sum.QuorumTime,
  225. TransferDetails: sum.TransferDetails,
  226. }
  227. if _, ok := row[quorumStateFam]; ok {
  228. item := row[quorumStateFam][0]
  229. deets.SignedVAA, _ = vaa.Unmarshal(item.Value)
  230. }
  231. if _, ok := row[transferPayloadFam]; ok {
  232. tokenTransferPayload := &TokenTransferPayload{}
  233. for _, item := range row[transferPayloadFam] {
  234. switch item.Column {
  235. case "TokenTransferPayload:Amount":
  236. tokenTransferPayload.Amount = string(item.Value)
  237. case "TokenTransferPayload:OriginAddress":
  238. tokenTransferPayload.OriginAddress = string(item.Value)
  239. case "TokenTransferPayload:OriginChain":
  240. tokenTransferPayload.OriginChain = string(item.Value)
  241. case "TokenTransferPayload:TargetAddress":
  242. tokenTransferPayload.TargetAddress = string(item.Value)
  243. case "TokenTransferPayload:TargetChain":
  244. tokenTransferPayload.TargetChain = string(item.Value)
  245. }
  246. }
  247. deets.TokenTransferPayload = tokenTransferPayload
  248. }
  249. if _, ok := row[metaPayloadFam]; ok {
  250. assetMetaPayload := &AssetMetaPayload{}
  251. for _, item := range row[metaPayloadFam] {
  252. switch item.Column {
  253. case "AssetMetaPayload:TokenAddress":
  254. assetMetaPayload.TokenAddress = string(item.Value)
  255. case "AssetMetaPayload:TokenChain":
  256. assetMetaPayload.TokenChain = string(item.Value)
  257. case "AssetMetaPayload:Decimals":
  258. assetMetaPayload.Decimals = string(item.Value)
  259. case "AssetMetaPayload:Symbol":
  260. assetMetaPayload.Symbol = string(item.Value)
  261. case "AssetMetaPayload:Name":
  262. assetMetaPayload.Name = string(item.Value)
  263. case "AssetMetaPayload:CoinGeckoCoinId":
  264. assetMetaPayload.CoinGeckoCoinId = string(item.Value)
  265. case "AssetMetaPayload:NativeAddress":
  266. assetMetaPayload.NativeAddress = string(item.Value)
  267. }
  268. }
  269. deets.AssetMetaPayload = assetMetaPayload
  270. }
  271. if _, ok := row[nftPayloadFam]; ok {
  272. nftTransferPayload := &NFTTransferPayload{}
  273. for _, item := range row[nftPayloadFam] {
  274. switch item.Column {
  275. case "NFTTransferPayload:OriginAddress":
  276. nftTransferPayload.OriginAddress = string(item.Value)
  277. case "NFTTransferPayload:OriginChain":
  278. nftTransferPayload.OriginChain = string(item.Value)
  279. case "NFTTransferPayload:Symbol":
  280. nftTransferPayload.Symbol = string(item.Value)
  281. case "NFTTransferPayload:Name":
  282. nftTransferPayload.Name = string(item.Value)
  283. case "NFTTransferPayload:TokenId":
  284. nftTransferPayload.TokenId = string(item.Value)
  285. case "NFTTransferPayload:URI":
  286. nftTransferPayload.URI = string(TrimUnicodeFromByteArray(item.Value))
  287. case "NFTTransferPayload:TargetAddress":
  288. nftTransferPayload.TargetAddress = string(item.Value)
  289. case "NFTTransferPayload:TargetChain":
  290. nftTransferPayload.TargetChain = string(item.Value)
  291. }
  292. }
  293. deets.NFTTransferPayload = nftTransferPayload
  294. }
  295. // NotionalUSD and TokenPriceUSD are more percise than the string versions returned,
  296. // however the precision is not required, so leaving this commented out for now.
  297. // if _, ok := row[transferDetailsFam]; ok {
  298. // for _, item := range row[transferDetailsFam] {
  299. // switch item.Column {
  300. // case "TokenTransferDetails:NotionalUSD":
  301. // reader := bytes.NewReader(item.Value)
  302. // var notionalUSD uint64
  303. // if err := binary.Read(reader, binary.BigEndian, &notionalUSD); err != nil {
  304. // log.Fatalf("failed to read NotionalUSD of row: %v. err %v ", row.Key(), err)
  305. // }
  306. // deets.TransferDetails.NotionalUSD = notionalUSD
  307. // case "TokenTransferDetails:TokenPriceUSD":
  308. // reader := bytes.NewReader(item.Value)
  309. // var tokenPriceUSD uint64
  310. // if err := binary.Read(reader, binary.BigEndian, &tokenPriceUSD); err != nil {
  311. // log.Fatalf("failed to read TokenPriceUSD of row: %v. err %v", row.Key(), err)
  312. // }
  313. // deets.TransferDetails.TokenPriceUSD = tokenPriceUSD
  314. // }
  315. // }
  316. // }
  317. if _, ok := row[chainDetailsFam]; ok {
  318. chainDetails := &ChainDetails{}
  319. for _, item := range row[chainDetailsFam] {
  320. switch item.Column {
  321. case "ChainDetails:SenderAddress":
  322. chainDetails.SenderAddress = string(item.Value)
  323. case "ChainDetails:ReceiverAddress":
  324. chainDetails.ReceiverAddress = string(item.Value)
  325. }
  326. }
  327. deets.ChainDetails = chainDetails
  328. }
  329. return deets
  330. }
  331. func roundToTwoDecimalPlaces(num float64) float64 {
  332. return math.Round(num*100) / 100
  333. }
  334. func createCachePrefix(prefix string) string {
  335. cachePrefix := prefix
  336. if prefix == "" {
  337. cachePrefix = "*"
  338. }
  339. return cachePrefix
  340. }
  341. var mux = newMux()
  342. // Entry is the cloud function entry point
  343. func Entry(w http.ResponseWriter, r *http.Request) {
  344. mux.ServeHTTP(w, r)
  345. }
  346. func newMux() *http.ServeMux {
  347. mux := http.NewServeMux()
  348. mux.HandleFunc("/notionaltransferred", NotionalTransferred)
  349. mux.HandleFunc("/notionaltransferredto", NotionalTransferredTo)
  350. mux.HandleFunc("/totals", Totals)
  351. mux.HandleFunc("/recent", Recent)
  352. mux.HandleFunc("/transaction", Transaction)
  353. mux.HandleFunc("/readrow", ReadRow)
  354. mux.HandleFunc("/findvalues", FindValues)
  355. mux.HandleFunc("/readyz", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })
  356. return mux
  357. }