2021-11-19 12:25:46 -08:00
// Package p contains an HTTP Cloud Function.
package p
import (
2022-03-02 14:37:05 -08:00
"context"
2021-11-19 12:25:46 -08:00
"encoding/json"
"fmt"
"io"
"log"
"net/http"
2022-03-02 14:37:05 -08:00
"sort"
"strconv"
"time"
2021-11-19 12:25:46 -08:00
"cloud.google.com/go/bigtable"
)
// fetch rows by matching payload value
func FindValues ( w http . ResponseWriter , r * http . Request ) {
// Set CORS headers for the preflight request
if r . Method == http . MethodOptions {
w . Header ( ) . Set ( "Access-Control-Allow-Origin" , "*" )
w . Header ( ) . Set ( "Access-Control-Allow-Methods" , "POST" )
w . Header ( ) . Set ( "Access-Control-Allow-Headers" , "Content-Type" )
w . Header ( ) . Set ( "Access-Control-Max-Age" , "3600" )
w . WriteHeader ( http . StatusNoContent )
return
}
// Set CORS headers for the main request.
w . Header ( ) . Set ( "Access-Control-Allow-Origin" , "*" )
2022-03-02 14:37:05 -08:00
var columnFamily , columnName , value , emitterChain , emitterAddress , vaaBytes , numRows string
2021-11-19 12:25:46 -08:00
// allow GET requests with querystring params, or POST requests with json body.
switch r . Method {
case http . MethodGet :
queryParams := r . URL . Query ( )
columnFamily = queryParams . Get ( "columnFamily" )
columnName = queryParams . Get ( "columnName" )
value = queryParams . Get ( "value" )
emitterChain = queryParams . Get ( "emitterChain" )
emitterAddress = queryParams . Get ( "emitterAddress" )
2022-03-02 14:37:05 -08:00
vaaBytes = queryParams . Get ( "vaaBytes" )
numRows = queryParams . Get ( "numRows" )
2021-11-19 12:25:46 -08:00
// check for empty values
if columnFamily == "" || columnName == "" || value == "" {
fmt . Fprint ( w , "query params ['columnFamily', 'columnName', 'value'] cannot be empty" )
http . Error ( w , http . StatusText ( http . StatusBadRequest ) , http . StatusBadRequest )
return
}
case http . MethodPost :
// declare request body properties
var d struct {
ColumnFamily string ` json:"columnFamily" `
ColumnName string ` json:"columnName" `
Value string ` json:"value" `
EmitterChain string ` json:"emitterChain" `
EmitterAddress string ` json:"emitterAddress" `
2022-03-02 14:37:05 -08:00
VAABytes string ` json:"vaaBytes" `
NumRows string ` json:"numRows" `
2021-11-19 12:25:46 -08:00
}
// deserialize request body
if err := json . NewDecoder ( r . Body ) . Decode ( & d ) ; err != nil {
switch err {
case io . EOF :
fmt . Fprint ( w , "request body required" )
return
default :
log . Printf ( "json.NewDecoder: %v" , err )
http . Error ( w , http . StatusText ( http . StatusBadRequest ) , http . StatusBadRequest )
return
}
}
// check for empty values
if d . ColumnFamily == "" || d . ColumnName == "" || d . Value == "" {
fmt . Fprint ( w , "body values ['columnFamily', 'columnName', 'value'] cannot be empty" )
http . Error ( w , http . StatusText ( http . StatusBadRequest ) , http . StatusBadRequest )
return
}
columnFamily = d . ColumnFamily
columnName = d . ColumnName
value = d . Value
emitterChain = d . EmitterChain
emitterAddress = d . EmitterAddress
2022-03-02 14:37:05 -08:00
vaaBytes = d . VAABytes
numRows = d . NumRows
2021-11-19 12:25:46 -08:00
default :
http . Error ( w , "405 - Method Not Allowed" , http . StatusMethodNotAllowed )
log . Println ( "Method Not Allowed" )
return
}
2022-03-02 14:37:05 -08:00
var resultCount uint64
if numRows == "" {
resultCount = 0
} else {
var convErr error
resultCount , convErr = strconv . ParseUint ( numRows , 10 , 64 )
if convErr != nil {
fmt . Fprint ( w , "numRows must be an integer" )
http . Error ( w , http . StatusText ( http . StatusBadRequest ) , http . StatusBadRequest )
return
}
}
2021-11-19 12:25:46 -08:00
if columnFamily != "TokenTransferPayload" &&
columnFamily != "AssetMetaPayload" &&
columnFamily != "NFTTransferPayload" &&
2022-03-02 14:37:05 -08:00
columnFamily != "TokenTransferDetails" &&
columnFamily != "ChainDetails" {
fmt . Fprint ( w , "columnFamily must be one of: ['TokenTransferPayload', 'AssetMetaPayload', 'NFTTransferPayload', 'TokenTransferDetails', 'ChainDetails']" )
2021-11-19 12:25:46 -08:00
http . Error ( w , http . StatusText ( http . StatusBadRequest ) , http . StatusBadRequest )
return
}
prefix := ""
if emitterChain != "" {
prefix = emitterChain
if emitterAddress != "" {
prefix = emitterChain + emitterAddress
}
}
2022-03-02 14:37:05 -08:00
ctx , cancel := context . WithTimeout ( context . Background ( ) , 60 * time . Second )
defer cancel ( )
2021-11-19 12:25:46 -08:00
results := [ ] bigtable . Row { }
2022-03-02 14:37:05 -08:00
err := tbl . ReadRows ( ctx , bigtable . PrefixRange ( prefix ) , func ( row bigtable . Row ) bool {
2021-11-19 12:25:46 -08:00
results = append ( results , row )
return true
} , bigtable . RowFilter (
bigtable . ConditionFilter (
bigtable . ChainFilters (
bigtable . FamilyFilter ( columnFamily ) ,
bigtable . ColumnFilter ( columnName ) ,
bigtable . ValueFilter ( value ) ,
) ,
bigtable . ChainFilters (
bigtable . PassAllFilter ( ) ,
bigtable . LatestNFilter ( 1 ) ,
) ,
bigtable . BlockAllFilter ( ) ,
) ) )
if err != nil {
http . Error ( w , "Error reading rows" , http . StatusInternalServerError )
log . Printf ( "tbl.ReadRows(): %v" , err )
return
}
2022-03-02 14:37:05 -08:00
if resultCount > 0 {
// means do not limit, cause you'd never query 0 rows.
// if the result set is limited to a number, sort the results
// and return the n latest.
// sort the results to be newest first
sort . Slice ( results , func ( i , j int ) bool {
// bigtable rows dont have timestamps, use a cell timestamp all rows will have.
var iTimestamp bigtable . Timestamp
var jTimestamp bigtable . Timestamp
// rows may have: only MessagePublication, only QuorumState, or both.
// find a timestamp for each row, try to use MessagePublication, if it exists:
if len ( results [ i ] [ "MessagePublication" ] ) >= 1 {
iTimestamp = results [ i ] [ "MessagePublication" ] [ 0 ] . Timestamp
} else if len ( results [ i ] [ "QuorumState" ] ) >= 1 {
iTimestamp = results [ i ] [ "QuorumState" ] [ 0 ] . Timestamp
}
if len ( results [ j ] [ "MessagePublication" ] ) >= 1 {
jTimestamp = results [ j ] [ "MessagePublication" ] [ 0 ] . Timestamp
} else if len ( results [ j ] [ "QuorumState" ] ) >= 1 {
jTimestamp = results [ j ] [ "QuorumState" ] [ 0 ] . Timestamp
}
return iTimestamp > jTimestamp
} )
// trim the result down to the requested amount
num := uint64 ( len ( results ) )
if num > resultCount {
results = results [ : resultCount ]
} else {
results = results [ : ]
}
}
2021-11-19 12:25:46 -08:00
details := [ ] Details { }
for _ , result := range results {
detail := makeDetails ( result )
2022-03-02 14:37:05 -08:00
// create a slimmer version of the details struct
slimDetails := Details {
Summary : Summary {
EmitterChain : detail . EmitterChain ,
EmitterAddress : detail . EmitterAddress ,
Sequence : detail . Sequence ,
InitiatingTxID : detail . InitiatingTxID ,
Payload : detail . Payload ,
QuorumTime : detail . QuorumTime ,
TransferDetails : detail . TransferDetails ,
} ,
TokenTransferPayload : detail . TokenTransferPayload ,
AssetMetaPayload : detail . AssetMetaPayload ,
NFTTransferPayload : detail . NFTTransferPayload ,
ChainDetails : detail . ChainDetails ,
}
if vaaBytes != "" {
slimDetails . SignedVAABytes = detail . SignedVAABytes
}
details = append ( details , slimDetails )
2021-11-19 12:25:46 -08:00
}
jsonBytes , err := json . Marshal ( details )
if err != nil {
w . WriteHeader ( http . StatusInternalServerError )
w . Write ( [ ] byte ( err . Error ( ) ) )
log . Println ( err . Error ( ) )
return
}
w . WriteHeader ( http . StatusOK )
w . Write ( jsonBytes )
}