585 lines
16 KiB
Go
585 lines
16 KiB
Go
package chains
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/wormhole-foundation/wormhole-explorer/common/domain"
|
|
"github.com/wormhole-foundation/wormhole-explorer/common/pool"
|
|
"github.com/wormhole-foundation/wormhole-explorer/txtracker/internal/metrics"
|
|
sdk "github.com/wormhole-foundation/wormhole/sdk/vaa"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
type apiWormchain struct {
|
|
p2pNetwork string
|
|
evmosPool *pool.Pool
|
|
kujiraPool *pool.Pool
|
|
osmosisPool *pool.Pool
|
|
}
|
|
|
|
type wormchainTxDetail struct {
|
|
Jsonrpc string `json:"jsonrpc"`
|
|
ID int `json:"id"`
|
|
Result struct {
|
|
Hash string `json:"hash"`
|
|
Height string `json:"height"`
|
|
Index int `json:"index"`
|
|
TxResult struct {
|
|
Code int `json:"code"`
|
|
Data string `json:"data"`
|
|
Log string `json:"log"`
|
|
Info string `json:"info"`
|
|
GasWanted string `json:"gas_wanted"`
|
|
GasUsed string `json:"gas_used"`
|
|
Events []struct {
|
|
Type string `json:"type"`
|
|
Attributes []struct {
|
|
Key string `json:"key"`
|
|
Value string `json:"value"`
|
|
Index bool `json:"index"`
|
|
} `json:"attributes"`
|
|
} `json:"events"`
|
|
Codespace string `json:"codespace"`
|
|
} `json:"tx_result"`
|
|
Tx string `json:"tx"`
|
|
} `json:"result"`
|
|
}
|
|
|
|
type event struct {
|
|
Type string `json:"type"`
|
|
Attributes []struct {
|
|
Key string `json:"key"`
|
|
Value string `json:"value"`
|
|
} `json:"attributes"`
|
|
}
|
|
|
|
type packetData struct {
|
|
Sender string `json:"sender"`
|
|
Receiver string `json:"receiver"`
|
|
}
|
|
|
|
type logWrapper struct {
|
|
Events []event `json:"events"`
|
|
}
|
|
|
|
type wormchainTx struct {
|
|
srcChannel, dstChannel, sender, receiver, timestamp, sequence string
|
|
}
|
|
|
|
func fetchWormchainDetail(ctx context.Context, baseUrl string, txHash string) (*wormchainTx, error) {
|
|
uri := fmt.Sprintf("%s/tx?hash=%s", baseUrl, txHash)
|
|
body, err := httpGet(ctx, uri)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var tx wormchainTxDetail
|
|
err = json.Unmarshal(body, &tx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var log []logWrapper
|
|
err = json.Unmarshal([]byte(tx.Result.TxResult.Log), &log)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var srcChannel, dstChannel, sender, receiver, timestamp, sequence string
|
|
for _, l := range log {
|
|
for _, e := range l.Events {
|
|
if e.Type == "recv_packet" {
|
|
for _, attr := range e.Attributes {
|
|
if attr.Key == "packet_src_channel" {
|
|
srcChannel = attr.Value
|
|
}
|
|
if attr.Key == "packet_dst_channel" {
|
|
dstChannel = attr.Value
|
|
}
|
|
if attr.Key == "packet_timeout_timestamp" {
|
|
timestamp = attr.Value
|
|
}
|
|
|
|
if attr.Key == "packet_sequence" {
|
|
sequence = attr.Value
|
|
}
|
|
|
|
if attr.Key == "packet_data" {
|
|
var pd packetData
|
|
err = json.Unmarshal([]byte(attr.Value), &pd)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
sender = pd.Sender
|
|
receiver = pd.Receiver
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return &wormchainTx{
|
|
srcChannel: srcChannel,
|
|
dstChannel: dstChannel,
|
|
sender: sender,
|
|
receiver: receiver,
|
|
timestamp: timestamp,
|
|
sequence: sequence,
|
|
}, nil
|
|
|
|
}
|
|
|
|
type osmosisRequest struct {
|
|
Jsonrpc string `json:"jsonrpc"`
|
|
ID int `json:"id"`
|
|
Method string `json:"method"`
|
|
Params struct {
|
|
Query string `json:"query"`
|
|
Page string `json:"page"`
|
|
} `json:"params"`
|
|
}
|
|
|
|
type osmosisResponse struct {
|
|
Jsonrpc string `json:"jsonrpc"`
|
|
ID int `json:"id"`
|
|
Result struct {
|
|
Txs []struct {
|
|
Hash string `json:"hash"`
|
|
Height string `json:"height"`
|
|
Index int `json:"index"`
|
|
TxResult struct {
|
|
Code int `json:"code"`
|
|
Data string `json:"data"`
|
|
Log string `json:"log"`
|
|
Info string `json:"info"`
|
|
GasWanted string `json:"gas_wanted"`
|
|
GasUsed string `json:"gas_used"`
|
|
Events []struct {
|
|
Type string `json:"type"`
|
|
Attributes []struct {
|
|
Key string `json:"key"`
|
|
Value string `json:"value"`
|
|
Index bool `json:"index"`
|
|
} `json:"attributes"`
|
|
} `json:"events"`
|
|
Codespace string `json:"codespace"`
|
|
} `json:"tx_result"`
|
|
Tx string `json:"tx"`
|
|
} `json:"txs"`
|
|
TotalCount string `json:"total_count"`
|
|
} `json:"result"`
|
|
}
|
|
|
|
type osmosisTx struct {
|
|
txHash string
|
|
}
|
|
|
|
func (a *apiWormchain) fetchOsmosisDetail(ctx context.Context, pool *pool.Pool, sequence, timestamp, srcChannel, dstChannel string, metrics metrics.Metrics) (*osmosisTx, error) {
|
|
if pool == nil {
|
|
return nil, fmt.Errorf("osmosis rpc pool not found")
|
|
}
|
|
|
|
osmosisRpcs := pool.GetItems()
|
|
if len(osmosisRpcs) == 0 {
|
|
return nil, fmt.Errorf("osmosis rpcs not found")
|
|
}
|
|
|
|
for _, rpc := range osmosisRpcs {
|
|
rpc.Wait(ctx)
|
|
osmosisTx, err := fetchOsmosisDetail(ctx, rpc.Id, sequence, timestamp, srcChannel, dstChannel)
|
|
if osmosisTx != nil {
|
|
metrics.IncCallRpcSuccess(uint16(sdk.ChainIDOsmosis), rpc.Description)
|
|
return osmosisTx, nil
|
|
}
|
|
if err != nil {
|
|
metrics.IncCallRpcError(uint16(sdk.ChainIDOsmosis), rpc.Description)
|
|
}
|
|
}
|
|
|
|
return nil, fmt.Errorf("osmosis tx not found")
|
|
}
|
|
|
|
func fetchOsmosisDetail(ctx context.Context, baseUrl string, sequence, timestamp, srcChannel, dstChannel string) (*osmosisTx, error) {
|
|
queryTemplate := `send_packet.packet_sequence='%s' AND send_packet.packet_timeout_timestamp='%s' AND send_packet.packet_src_channel='%s' AND send_packet.packet_dst_channel='%s'`
|
|
query := fmt.Sprintf(queryTemplate, sequence, timestamp, srcChannel, dstChannel)
|
|
q := osmosisRequest{
|
|
Jsonrpc: "2.0",
|
|
ID: 1,
|
|
Method: "tx_search",
|
|
Params: struct {
|
|
Query string `json:"query"`
|
|
Page string `json:"page"`
|
|
}{
|
|
Query: query,
|
|
Page: "1",
|
|
},
|
|
}
|
|
|
|
response, err := httpPost(ctx, baseUrl, q)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var oReponse osmosisResponse
|
|
err = json.Unmarshal(response, &oReponse)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(oReponse.Result.Txs) == 0 {
|
|
return nil, fmt.Errorf("can not found hash for sequence %s, timestamp %s, srcChannel %s, dstChannel %s", sequence, timestamp, srcChannel, dstChannel)
|
|
}
|
|
return &osmosisTx{txHash: strings.ToLower(oReponse.Result.Txs[0].Hash)}, nil
|
|
}
|
|
|
|
type evmosRequest struct {
|
|
Jsonrpc string `json:"jsonrpc"`
|
|
ID int `json:"id"`
|
|
Method string `json:"method"`
|
|
Params struct {
|
|
Query string `json:"query"`
|
|
Page string `json:"page"`
|
|
} `json:"params"`
|
|
}
|
|
|
|
type evmosResponse struct {
|
|
Jsonrpc string `json:"jsonrpc"`
|
|
ID int `json:"id"`
|
|
Result struct {
|
|
Txs []struct {
|
|
Hash string `json:"hash"`
|
|
Height string `json:"height"`
|
|
Index int `json:"index"`
|
|
TxResult struct {
|
|
Code int `json:"code"`
|
|
Data string `json:"data"`
|
|
Log string `json:"log"`
|
|
Info string `json:"info"`
|
|
GasWanted string `json:"gas_wanted"`
|
|
GasUsed string `json:"gas_used"`
|
|
Events []struct {
|
|
Type string `json:"type"`
|
|
Attributes []struct {
|
|
Key string `json:"key"`
|
|
Value string `json:"value"`
|
|
Index bool `json:"index"`
|
|
} `json:"attributes"`
|
|
} `json:"events"`
|
|
Codespace string `json:"codespace"`
|
|
} `json:"tx_result"`
|
|
Tx string `json:"tx"`
|
|
} `json:"txs"`
|
|
TotalCount string `json:"total_count"`
|
|
} `json:"result"`
|
|
}
|
|
|
|
type evmosTx struct {
|
|
txHash string
|
|
}
|
|
|
|
func (a *apiWormchain) fetchEvmosDetail(ctx context.Context, pool *pool.Pool, sequence, timestamp, srcChannel, dstChannel string, metrics metrics.Metrics) (*evmosTx, error) {
|
|
if pool == nil {
|
|
return nil, fmt.Errorf("evmos rpc pool not found")
|
|
}
|
|
evmosRpcs := pool.GetItems()
|
|
if len(evmosRpcs) == 0 {
|
|
return nil, fmt.Errorf("evmos rpcs not found")
|
|
}
|
|
|
|
for _, rpc := range evmosRpcs {
|
|
rpc.Wait(ctx)
|
|
evmosTx, err := fetchEvmosDetail(ctx, rpc.Id, sequence, timestamp, srcChannel, dstChannel)
|
|
if evmosTx != nil {
|
|
metrics.IncCallRpcSuccess(uint16(sdk.ChainIDEvmos), rpc.Description)
|
|
return evmosTx, nil
|
|
}
|
|
if err != nil {
|
|
metrics.IncCallRpcError(uint16(sdk.ChainIDEvmos), rpc.Description)
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("evmos tx not found")
|
|
|
|
}
|
|
|
|
func fetchEvmosDetail(ctx context.Context, baseUrl string, sequence, timestamp, srcChannel, dstChannel string) (*evmosTx, error) {
|
|
queryTemplate := `send_packet.packet_sequence='%s' AND send_packet.packet_timeout_timestamp='%s' AND send_packet.packet_src_channel='%s' AND send_packet.packet_dst_channel='%s'`
|
|
query := fmt.Sprintf(queryTemplate, sequence, timestamp, srcChannel, dstChannel)
|
|
q := evmosRequest{
|
|
Jsonrpc: "2.0",
|
|
ID: 1,
|
|
Method: "tx_search",
|
|
Params: struct {
|
|
Query string `json:"query"`
|
|
Page string `json:"page"`
|
|
}{
|
|
Query: query,
|
|
Page: "1",
|
|
},
|
|
}
|
|
|
|
//response, err := httpPost(ctx, rateLimiter, baseUrl, q)
|
|
response, err := httpPost(ctx, baseUrl, q)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var eReponse evmosResponse
|
|
err = json.Unmarshal(response, &eReponse)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(eReponse.Result.Txs) == 0 {
|
|
return nil, fmt.Errorf("can not found hash for sequence %s, timestamp %s, srcChannel %s, dstChannel %s", sequence, timestamp, srcChannel, dstChannel)
|
|
}
|
|
return &evmosTx{txHash: strings.ToLower(eReponse.Result.Txs[0].Hash)}, nil
|
|
}
|
|
|
|
type kujiraRequest struct {
|
|
Jsonrpc string `json:"jsonrpc"`
|
|
ID int `json:"id"`
|
|
Method string `json:"method"`
|
|
Params struct {
|
|
Query string `json:"query"`
|
|
Page string `json:"page"`
|
|
} `json:"params"`
|
|
}
|
|
|
|
type kujiraResponse struct {
|
|
Jsonrpc string `json:"jsonrpc"`
|
|
ID int `json:"id"`
|
|
Result struct {
|
|
Txs []struct {
|
|
Hash string `json:"hash"`
|
|
Height string `json:"height"`
|
|
Index int `json:"index"`
|
|
TxResult struct {
|
|
Code int `json:"code"`
|
|
Data string `json:"data"`
|
|
Log string `json:"log"`
|
|
Info string `json:"info"`
|
|
GasWanted string `json:"gas_wanted"`
|
|
GasUsed string `json:"gas_used"`
|
|
Events []struct {
|
|
Type string `json:"type"`
|
|
Attributes []struct {
|
|
Key string `json:"key"`
|
|
Value string `json:"value"`
|
|
Index bool `json:"index"`
|
|
} `json:"attributes"`
|
|
} `json:"events"`
|
|
Codespace string `json:"codespace"`
|
|
} `json:"tx_result"`
|
|
Tx string `json:"tx"`
|
|
} `json:"txs"`
|
|
TotalCount string `json:"total_count"`
|
|
} `json:"result"`
|
|
}
|
|
|
|
type kujiraTx struct {
|
|
txHash string
|
|
}
|
|
|
|
func (a *apiWormchain) fetchKujiraDetail(ctx context.Context, pool *pool.Pool, sequence, timestamp, srcChannel, dstChannel string, metrics metrics.Metrics) (*kujiraTx, error) {
|
|
if pool == nil {
|
|
return nil, fmt.Errorf("kujira rpc pool not found")
|
|
}
|
|
kujiraRpcs := pool.GetItems()
|
|
if len(kujiraRpcs) == 0 {
|
|
return nil, fmt.Errorf("kujira rpcs not found")
|
|
}
|
|
for _, rpc := range kujiraRpcs {
|
|
rpc.Wait(ctx)
|
|
kujiraTx, err := fetchKujiraDetail(ctx, rpc.Id, sequence, timestamp, srcChannel, dstChannel)
|
|
if kujiraTx != nil {
|
|
metrics.IncCallRpcSuccess(uint16(sdk.ChainIDKujira), rpc.Description)
|
|
return kujiraTx, nil
|
|
}
|
|
if err != nil {
|
|
metrics.IncCallRpcError(uint16(sdk.ChainIDKujira), rpc.Description)
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("kujira tx not found")
|
|
}
|
|
|
|
func fetchKujiraDetail(ctx context.Context, baseUrl string, sequence, timestamp, srcChannel, dstChannel string) (*kujiraTx, error) {
|
|
queryTemplate := `send_packet.packet_sequence='%s' AND send_packet.packet_timeout_timestamp='%s' AND send_packet.packet_src_channel='%s' AND send_packet.packet_dst_channel='%s'`
|
|
query := fmt.Sprintf(queryTemplate, sequence, timestamp, srcChannel, dstChannel)
|
|
q := kujiraRequest{
|
|
Jsonrpc: "2.0",
|
|
ID: 1,
|
|
Method: "tx_search",
|
|
Params: struct {
|
|
Query string `json:"query"`
|
|
Page string `json:"page"`
|
|
}{
|
|
Query: query,
|
|
Page: "1",
|
|
},
|
|
}
|
|
|
|
response, err := httpPost(ctx, baseUrl, q)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var kReponse kujiraResponse
|
|
err = json.Unmarshal(response, &kReponse)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(kReponse.Result.Txs) == 0 {
|
|
return nil, fmt.Errorf("can not found hash for sequence %s, timestamp %s, srcChannel %s, dstChannel %s", sequence, timestamp, srcChannel, dstChannel)
|
|
}
|
|
return &kujiraTx{txHash: strings.ToLower(kReponse.Result.Txs[0].Hash)}, nil
|
|
}
|
|
|
|
type WorchainAttributeTxDetail struct {
|
|
OriginChainID sdk.ChainID `bson:"originChainId"`
|
|
OriginTxHash string `bson:"originTxHash"`
|
|
OriginAddress string `bson:"originAddress"`
|
|
}
|
|
|
|
func (a *apiWormchain) FetchWormchainTx(
|
|
ctx context.Context,
|
|
wormchainPool *pool.Pool,
|
|
txHash string,
|
|
metrics metrics.Metrics,
|
|
logger *zap.Logger,
|
|
) (*TxDetail, error) {
|
|
|
|
txHash = txHashLowerCaseWith0x(txHash)
|
|
|
|
// Get the wormchain rpcs sorted by availability.
|
|
wormchainRpcs := wormchainPool.GetItems()
|
|
if len(wormchainRpcs) == 0 {
|
|
return nil, errors.New("wormchain rpc pool is empty")
|
|
}
|
|
|
|
var wormchainTx *wormchainTx
|
|
var err error
|
|
for _, rpc := range wormchainRpcs {
|
|
// wait for the rpc to be available
|
|
rpc.Wait(ctx)
|
|
wormchainTx, err = fetchWormchainDetail(ctx, rpc.Id, txHash)
|
|
if err != nil {
|
|
metrics.IncCallRpcError(uint16(sdk.ChainIDWormchain), rpc.Description)
|
|
logger.Debug("Failed to fetch transaction from wormchain", zap.String("url", rpc.Id), zap.Error(err))
|
|
continue
|
|
}
|
|
metrics.IncCallRpcSuccess(uint16(sdk.ChainIDWormchain), rpc.Description)
|
|
break
|
|
}
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if wormchainTx == nil {
|
|
return nil, errors.New("failed to fetch wormchain transaction details")
|
|
}
|
|
|
|
// Verify if this transaction is from osmosis by wormchain
|
|
if a.isOsmosisTx(wormchainTx) {
|
|
osmosisTx, err := a.fetchOsmosisDetail(ctx, a.osmosisPool, wormchainTx.sequence, wormchainTx.timestamp, wormchainTx.srcChannel, wormchainTx.dstChannel, metrics)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &TxDetail{
|
|
NativeTxHash: txHash,
|
|
From: wormchainTx.receiver,
|
|
Attribute: &AttributeTxDetail{
|
|
Type: "wormchain-gateway",
|
|
Value: &WorchainAttributeTxDetail{
|
|
OriginChainID: sdk.ChainIDOsmosis,
|
|
OriginTxHash: osmosisTx.txHash,
|
|
OriginAddress: wormchainTx.sender,
|
|
},
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// Verify if this transaction is from kujira by wormchain
|
|
if a.isKujiraTx(wormchainTx) {
|
|
kujiraTx, err := a.fetchKujiraDetail(ctx, a.kujiraPool, wormchainTx.sequence, wormchainTx.timestamp, wormchainTx.srcChannel, wormchainTx.dstChannel, metrics)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &TxDetail{
|
|
NativeTxHash: txHash,
|
|
From: wormchainTx.receiver,
|
|
Attribute: &AttributeTxDetail{
|
|
Type: "wormchain-gateway",
|
|
Value: &WorchainAttributeTxDetail{
|
|
OriginChainID: sdk.ChainIDKujira,
|
|
OriginTxHash: kujiraTx.txHash,
|
|
OriginAddress: wormchainTx.sender,
|
|
},
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
// Verify if this transaction is from evmos by wormchain
|
|
if a.isEvmosTx(wormchainTx) {
|
|
evmosTx, err := a.fetchEvmosDetail(ctx, a.evmosPool, wormchainTx.sequence, wormchainTx.timestamp, wormchainTx.srcChannel, wormchainTx.dstChannel, metrics)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &TxDetail{
|
|
NativeTxHash: txHash,
|
|
From: wormchainTx.receiver,
|
|
Attribute: &AttributeTxDetail{
|
|
Type: "wormchain-gateway",
|
|
Value: &WorchainAttributeTxDetail{
|
|
OriginChainID: sdk.ChainIDEvmos,
|
|
OriginTxHash: evmosTx.txHash,
|
|
OriginAddress: wormchainTx.sender,
|
|
},
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
return &TxDetail{
|
|
NativeTxHash: txHash,
|
|
From: wormchainTx.receiver,
|
|
}, nil
|
|
}
|
|
|
|
func (a *apiWormchain) isOsmosisTx(tx *wormchainTx) bool {
|
|
if a.p2pNetwork == domain.P2pMainNet {
|
|
return tx.srcChannel == "channel-2186" && tx.dstChannel == "channel-3"
|
|
}
|
|
if a.p2pNetwork == domain.P2pTestNet {
|
|
return tx.srcChannel == "channel-3086" && tx.dstChannel == "channel-5"
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (a *apiWormchain) isKujiraTx(tx *wormchainTx) bool {
|
|
if a.p2pNetwork == domain.P2pMainNet {
|
|
return tx.srcChannel == "channel-113" && tx.dstChannel == "channel-9"
|
|
}
|
|
// Pending get channels for testnet
|
|
// if a.p2pNetwork == domain.P2pTestNet {
|
|
// return tx.srcChannel == "" && tx.dstChannel == ""
|
|
// }
|
|
return false
|
|
}
|
|
|
|
func (a *apiWormchain) isEvmosTx(tx *wormchainTx) bool {
|
|
if a.p2pNetwork == domain.P2pMainNet {
|
|
return tx.srcChannel == "channel-94" && tx.dstChannel == "channel-5"
|
|
}
|
|
// Pending get channels for testnet
|
|
// if a.p2pNetwork == domain.P2pTestNet {
|
|
// return tx.srcChannel == "" && tx.dstChannel == ""
|
|
// }
|
|
return false
|
|
}
|