├── version ├── version_number.txt └── .gitignore ├── test_screenshots ├── check_data.png └── check_construction.png ├── config.yaml ├── cmd └── theta-rosetta-rpc-adaptor │ ├── main.go │ └── cmds │ ├── start.go │ └── root.go ├── .gitignore ├── bootstrap_balances.json ├── go.mod ├── node └── node.go ├── common ├── kvstore.go ├── config.go ├── db.go ├── types.go ├── errors.go ├── stake_service.go └── utils.go ├── Makefile ├── Dockerfile ├── cli-test-config.json ├── run.sh ├── README.md ├── services ├── server.go ├── mempool.go ├── network.go ├── block.go ├── account.go └── construction.go └── theta.ros /version/version_number.txt: -------------------------------------------------------------------------------- 1 | 0.0.1 2 | -------------------------------------------------------------------------------- /version/.gitignore: -------------------------------------------------------------------------------- 1 | version_generated.go 2 | -------------------------------------------------------------------------------- /test_screenshots/check_data.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thetatoken/theta-rosetta-rpc-adaptor/HEAD/test_screenshots/check_data.png -------------------------------------------------------------------------------- /test_screenshots/check_construction.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thetatoken/theta-rosetta-rpc-adaptor/HEAD/test_screenshots/check_construction.png -------------------------------------------------------------------------------- /config.yaml: -------------------------------------------------------------------------------- 1 | theta: 2 | rpcEndpoint: "http://127.0.0.1:16888/rpc" 3 | rpc: 4 | httpAddress: "127.0.0.1" 5 | httpPort: 8080 6 | log: 7 | levels: "*:debug" -------------------------------------------------------------------------------- /cmd/theta-rosetta-rpc-adaptor/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | cmdroot "github.com/thetatoken/theta-rosetta-rpc-adaptor/cmd/theta-rosetta-rpc-adaptor/cmds" 5 | ) 6 | 7 | func main() { 8 | cmdroot.Execute() 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | -------------------------------------------------------------------------------- /bootstrap_balances.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "account_identifier": { 4 | "address": "0x2E833968E5bB786Ae419c4d13189fB081Cc43bab" 5 | }, 6 | "currency": { 7 | "symbol": "thetawei", 8 | "decimals": 18 9 | }, 10 | "value": "12500000000000000000000000" 11 | }, 12 | { 13 | "account_identifier": { 14 | "address": "0x2E833968E5bB786Ae419c4d13189fB081Cc43bab" 15 | }, 16 | "currency": { 17 | "symbol": "tfuelwei", 18 | "decimals": 18 19 | }, 20 | "value": "312500000000000000000000000" 21 | } 22 | ] -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/thetatoken/theta-rosetta-rpc-adaptor 2 | 3 | require ( 4 | github.com/coinbase/rosetta-sdk-go v0.6.10 5 | github.com/dgraph-io/badger v1.6.1 // indirect 6 | github.com/mitchellh/go-homedir v1.1.0 7 | github.com/sirupsen/logrus v1.6.0 8 | github.com/spf13/cobra v1.1.1 9 | github.com/spf13/viper v1.7.0 10 | github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect 11 | github.com/thetatoken/theta v0.0.0 12 | github.com/thetatoken/theta/common v0.0.0 13 | github.com/ybbus/jsonrpc v2.1.2+incompatible 14 | ) 15 | 16 | replace github.com/thetatoken/theta v0.0.0 => ../theta 17 | 18 | replace github.com/thetatoken/theta/common v0.0.0 => ../theta/common 19 | 20 | replace github.com/thetatoken/theta/rpc/lib/rpc-codec/jsonrpc2 v0.0.0 => ../theta/rpc/lib/rpc-codec/jsonrpc2/ 21 | 22 | replace github.com/ethereum/go-ethereum => github.com/ethereum/go-ethereum v1.10.9 23 | 24 | go 1.13 25 | -------------------------------------------------------------------------------- /node/node.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | 7 | "github.com/thetatoken/theta-rosetta-rpc-adaptor/services" 8 | // erpclib "github.com/ethereum/go-ethereum/rpc" 9 | ) 10 | 11 | type Node struct { 12 | // Life cycle 13 | wg *sync.WaitGroup 14 | quit chan struct{} 15 | ctx context.Context 16 | cancel context.CancelFunc 17 | stopped bool 18 | } 19 | 20 | func NewNode() *Node { 21 | node := &Node{ 22 | wg: &sync.WaitGroup{}, 23 | } 24 | 25 | return node 26 | } 27 | 28 | // Start starts sub components and kick off the main loop. 29 | func (n *Node) Start(ctx context.Context) { 30 | c, cancel := context.WithCancel(ctx) 31 | n.ctx = c 32 | n.cancel = cancel 33 | 34 | services.StartServers() 35 | 36 | n.wg.Add(1) 37 | go n.mainLoop() 38 | } 39 | 40 | // Stop notifies all sub components to stop without blocking. 41 | func (n *Node) Stop() { 42 | n.cancel() 43 | 44 | services.StopServers() 45 | } 46 | 47 | // Wait blocks until all sub components stop. 48 | func (n *Node) Wait() { 49 | n.wg.Wait() 50 | } 51 | 52 | func (n *Node) mainLoop() { 53 | defer n.wg.Done() 54 | 55 | <-n.ctx.Done() 56 | n.stopped = true 57 | } 58 | -------------------------------------------------------------------------------- /cmd/theta-rosetta-rpc-adaptor/cmds/start.go: -------------------------------------------------------------------------------- 1 | package cmds 2 | 3 | import ( 4 | "context" 5 | _ "net/http/pprof" 6 | "os" 7 | "os/signal" 8 | "time" 9 | 10 | log "github.com/sirupsen/logrus" 11 | "github.com/spf13/cobra" 12 | 13 | "github.com/thetatoken/theta-rosetta-rpc-adaptor/node" 14 | ) 15 | 16 | // startCmd represents the start command 17 | var startCmd = &cobra.Command{ 18 | Use: "start", 19 | Short: "Start Theta Rosetta RPC Adaptor", 20 | Run: runStart, 21 | } 22 | 23 | func init() { 24 | RootCmd.AddCommand(startCmd) 25 | } 26 | 27 | func runStart(cmd *cobra.Command, args []string) { 28 | // trap Ctrl+C and call cancel on the context 29 | ctx, cancel := context.WithCancel(context.Background()) 30 | 31 | n := node.NewNode() 32 | 33 | c := make(chan os.Signal) 34 | signal.Notify(c, os.Interrupt) 35 | done := make(chan struct{}) 36 | go func() { 37 | <-c 38 | signal.Stop(c) 39 | cancel() 40 | // Wait at most 5 seconds before forcefully shutting down. 41 | <-time.After(time.Duration(5) * time.Second) 42 | close(done) 43 | }() 44 | 45 | n.Start(ctx) 46 | 47 | go func() { 48 | n.Wait() 49 | close(done) 50 | }() 51 | 52 | <-done 53 | log.Infof("") 54 | log.Infof("Graceful exit.") 55 | } 56 | -------------------------------------------------------------------------------- /common/kvstore.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "github.com/thetatoken/theta/common" 5 | "github.com/thetatoken/theta/rlp" 6 | ) 7 | 8 | type Store interface { 9 | Put(key common.Bytes, value interface{}) error 10 | Delete(key common.Bytes) error 11 | Get(key common.Bytes, value interface{}) error 12 | } 13 | 14 | // NewKVStore create a new instance of KVStore. 15 | func NewKVStore(db *LDBDatabase) Store { 16 | return &KVStore{db} 17 | } 18 | 19 | // KVStore a Database wrapped object. 20 | type KVStore struct { 21 | db *LDBDatabase 22 | } 23 | 24 | // Put upserts key/value into DB 25 | func (store *KVStore) Put(key common.Bytes, value interface{}) error { 26 | encodedValue, err := rlp.EncodeToBytes(value) 27 | if err != nil { 28 | return err 29 | } 30 | return store.db.Put(key, encodedValue) 31 | } 32 | 33 | // Delete deletes key entry from DB 34 | func (store *KVStore) Delete(key common.Bytes) error { 35 | return store.db.Delete(key) 36 | } 37 | 38 | // Get looks up DB with key and returns result into value (passed by reference) 39 | func (store *KVStore) Get(key common.Bytes, value interface{}) error { 40 | encodedValue, err := store.db.Get(key) 41 | if err != nil { 42 | return err 43 | } 44 | return rlp.DecodeBytes(encodedValue, value) 45 | } 46 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Theta Implementation of the Rosetta RPC API standard developed by Coinbase.com 2 | # https://blog.coinbase.com/introducing-rosetta-build-once-integrate-your-blockchain-everywhere-9b97d284f5b9 3 | 4 | GOTOOLS = github.com/mitchellh/gox \ 5 | github.com/Masterminds/glide \ 6 | github.com/rigelrozanski/shelldown/cmd/shelldown 7 | INCLUDE = -I=. -I=${GOPATH}/src -I=${GOPATH}/src/github.com/gogo/protobuf/protobuf 8 | 9 | all: install 10 | 11 | build: gen_version 12 | go build ./cmd/... 13 | 14 | # Build binaries for Linux platform. 15 | linux: gen_version 16 | integration/docker/build/build.sh force 17 | 18 | windows: 19 | CGO_ENABLED=1 GOOS=windows GOARCH=amd64 CC=x86_64-w64-mingw32-gcc CXX=x86_64-w64-mingw32-g++ go build -o build/windows/edgecore.exe ./cmd/edgecore 20 | 21 | docker: 22 | integration/docker/node/build.sh force 23 | 24 | install: gen_version release 25 | 26 | release: 27 | go install ./cmd/... 28 | 29 | debug: 30 | go install -race ./cmd/... 31 | 32 | clean: 33 | @rm -rf ./build 34 | 35 | gen_doc: 36 | cd ./docs/commands/;go build -o generator.exe; ./generator.exe 37 | 38 | BUILD_DATE := `date -u` 39 | GIT_HASH := `git rev-parse HEAD` 40 | VERSION_NUMER := `cat version/version_number.txt` 41 | VERSIONFILE := version/version_generated.go 42 | 43 | gen_version: 44 | @echo "package version" > $(VERSIONFILE) 45 | @echo "const (" >> $(VERSIONFILE) 46 | @echo " Timestamp = \"$(BUILD_DATE)\"" >> $(VERSIONFILE) 47 | @echo " Version = \"$(VERSION_NUMER)\"" >> $(VERSIONFILE) 48 | @echo " GitHash = \"$(GIT_HASH)\"" >> $(VERSIONFILE) 49 | @echo ")" >> $(VERSIONFILE) 50 | 51 | .PHONY: all build install clean 52 | -------------------------------------------------------------------------------- /cmd/theta-rosetta-rpc-adaptor/cmds/root.go: -------------------------------------------------------------------------------- 1 | package cmds 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path" 7 | "strings" 8 | 9 | homedir "github.com/mitchellh/go-homedir" 10 | "github.com/spf13/cobra" 11 | "github.com/spf13/viper" 12 | 13 | cmn "github.com/thetatoken/theta-rosetta-rpc-adaptor/common" 14 | ) 15 | 16 | var cfgPath string 17 | var mode string 18 | 19 | // RootCmd represents the base command when called without any subcommands 20 | var RootCmd = &cobra.Command{ 21 | Use: "theta-rosetta-rpc-adaptor", 22 | Short: "Theta Rosetta RPC Adaptor", 23 | Long: `Theta Rosetta RPC Adaptor`, 24 | } 25 | 26 | // Execute adds all child commands to the root command and sets flags appropriately. 27 | // This is called by main.main(). It only needs to happen once to the rootCmd. 28 | func Execute() { 29 | if err := RootCmd.Execute(); err != nil { 30 | fmt.Println(err) 31 | os.Exit(1) 32 | } 33 | } 34 | 35 | func init() { 36 | cobra.OnInitialize(initConfig) 37 | RootCmd.PersistentFlags().StringVar(&cfgPath, "config", getDefaultConfigPath(), fmt.Sprintf("config path (default is %s)", getDefaultConfigPath())) 38 | RootCmd.PersistentFlags().StringVar(&mode, "mode", "", "Rosetta mode (online/offline") 39 | viper.BindPFlag(cmn.CfgRosettaMode, RootCmd.PersistentFlags().Lookup("mode")) 40 | } 41 | 42 | // initConfig reads in config file and ENV variables if set. 43 | func initConfig() { 44 | viper.AddConfigPath(cfgPath) 45 | 46 | // Search config (without extension). 47 | viper.SetConfigName("config") 48 | 49 | viper.AutomaticEnv() // read in environment variables that match 50 | viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) 51 | 52 | // If a config file is found, read it in. 53 | if err := viper.ReadInConfig(); err == nil { 54 | fmt.Println("Using config file:", viper.ConfigFileUsed()) 55 | } 56 | } 57 | 58 | func getDefaultConfigPath() string { 59 | home, err := homedir.Dir() 60 | if err != nil { 61 | fmt.Println(err) 62 | os.Exit(1) 63 | } 64 | return path.Join(home, ".theta-rosetta-rpc-adaptor") 65 | } 66 | -------------------------------------------------------------------------------- /common/config.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "github.com/spf13/viper" 5 | ) 6 | 7 | const ( 8 | // CfgConfigPath defines custom config path 9 | CfgConfigPath = "config.path" 10 | 11 | // CfgThetaRPCEndpoint configures the Theta RPC endpoint 12 | CfgThetaRPCEndpoint = "theta.rpcEndpoint" 13 | 14 | // CfgRPCHttpAddress sets the binding address of RPC http service. 15 | CfgRPCHttpAddress = "rpc.httpAddress" 16 | // CfgRPCHttpPort sets the port of RPC http service. 17 | CfgRPCHttpPort = "rpc.httpPort" 18 | // CfgRPCWSAddress sets the binding address of RPC websocket service. 19 | CfgRPCWSAddress = "rpc.wsAddress" 20 | // CfgRPCWSPort sets the port of RPC websocket service. 21 | CfgRPCWSPort = "rpc.wsPort" 22 | // CfgRPCMaxConnections limits concurrent connections accepted by RPC server. 23 | CfgRPCMaxConnections = "rpc.maxConnections" 24 | // CfgRPCTimeoutSecs set a timeout for RPC. 25 | CfgRPCTimeoutSecs = "rpc.timeoutSecs" 26 | 27 | // CfgLogLevels sets the log level. 28 | CfgLogLevels = "log.levels" 29 | // CfgLogPrintSelfID determines whether to print node's ID in log (Useful in simulation when 30 | // there are more than one node running). 31 | CfgLogPrintSelfID = "log.printSelfID" 32 | 33 | CfgRosettaVersion = "rosetta.version" 34 | 35 | // CfgRosettaMode determines if the implementation is permitted to make outbound connections. 36 | CfgRosettaMode = "rosetta.mode" 37 | CfgRosettaModeOnline = "online" 38 | CfgRosettaModeOffline = "offline" 39 | ) 40 | 41 | func init() { 42 | viper.SetDefault(CfgThetaRPCEndpoint, "http://127.0.0.1:16888/rpc") 43 | 44 | viper.SetDefault(CfgRPCHttpAddress, "0.0.0.0") 45 | viper.SetDefault(CfgRPCHttpPort, "8080") 46 | viper.SetDefault(CfgRPCWSAddress, "0.0.0.0") 47 | viper.SetDefault(CfgRPCWSPort, "8081") 48 | viper.SetDefault(CfgRPCMaxConnections, 2048) 49 | viper.SetDefault(CfgRPCTimeoutSecs, 600) 50 | 51 | viper.SetDefault(CfgLogLevels, "*:debug") 52 | viper.SetDefault(CfgLogPrintSelfID, false) 53 | 54 | viper.SetDefault(CfgRosettaVersion, "1.1.1") 55 | viper.SetDefault(CfgRosettaMode, "online") 56 | } 57 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # ------------------------------------------------------------------------------ 2 | # Build Theta 3 | # ------------------------------------------------------------------------------ 4 | FROM golang:1.16.4 as service-builder 5 | 6 | ENV GO111MODULE=on 7 | ENV THETA_HOME=/go/src/github.com/thetatoken/theta 8 | 9 | WORKDIR $THETA_HOME 10 | 11 | RUN git clone https://github.com/thetatoken/theta-protocol-ledger.git . 12 | 13 | RUN make install 14 | 15 | # ------------------------------------------------------------------------------ 16 | # Build Theta Rosetta 17 | # ------------------------------------------------------------------------------ 18 | ENV ROSETTA_HOME=/go/src/github.com/thetatoken/theta-rosetta-rpc-adaptor 19 | 20 | WORKDIR $ROSETTA_HOME 21 | 22 | RUN git clone https://github.com/thetatoken/theta-rosetta-rpc-adaptor.git . 23 | 24 | RUN make install 25 | 26 | # ------------------------------------------------------------------------------ 27 | # Build final image 28 | # ------------------------------------------------------------------------------ 29 | FROM ubuntu:20.04 30 | 31 | RUN apt-get update && apt-get install -y ca-certificates && update-ca-certificates 32 | RUN apt-get -y install curl 33 | RUN apt-get -y install wget 34 | 35 | RUN mkdir -p /app \ 36 | && chown -R nobody:nogroup /app \ 37 | && mkdir -p /data \ 38 | && chown -R nobody:nogroup /data 39 | 40 | WORKDIR /app 41 | 42 | COPY --from=service-builder /go/src/github.com/thetatoken/theta/integration ./integration/ 43 | 44 | RUN mkdir -p ./mainnet/walletnode 45 | 46 | # Copy binary from theta-builder 47 | COPY --from=service-builder /go/bin/theta /app/theta 48 | COPY --from=service-builder /go/bin/thetacli /app/thetacli 49 | 50 | # # Copy binary from rosetta-builder 51 | COPY --from=service-builder /go/bin/theta-rosetta-rpc-adaptor /app/theta-rosetta-rpc-adaptor 52 | 53 | # Install service start script 54 | COPY --from=service-builder \ 55 | /go/src/github.com/thetatoken/theta-rosetta-rpc-adaptor/run.sh \ 56 | /app/run.sh 57 | 58 | # Set permissions for everything added to /app 59 | RUN chmod -R 755 /app/* 60 | 61 | EXPOSE 8080 62 | EXPOSE 15872 63 | EXPOSE 16888 64 | EXPOSE 21000 65 | EXPOSE 30001 66 | 67 | ENTRYPOINT [ "/app/run.sh" ] -------------------------------------------------------------------------------- /cli-test-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "network": { 3 | "blockchain": "theta", 4 | "network": "testnet" 5 | }, 6 | "online_url": "http://localhost:8080", 7 | "data_directory": "data", 8 | "http_timeout": 300, 9 | "max_retries": 15, 10 | "max_online_connections": 500, 11 | "max_sync_concurrency": 64, 12 | "tip_delay": 300, 13 | "compression_disabled": false, 14 | "memory_limit_disabled": false, 15 | "construction": { 16 | "offline_url": "http://localhost:8080", 17 | "stale_depth": 3, 18 | "broadcast_limit": 5, 19 | "constructor_dsl_file": "theta.ros", 20 | "end_conditions": { 21 | "create_account": 1, 22 | "transfer": 1, 23 | "smart_contract_transfer": 1 24 | }, 25 | "prefunded_accounts": [ 26 | { 27 | "privkey":"", 28 | "account_identifier":{ 29 | "address":"0x25302460651DA8D052ed1a405D6f2B6861445e48", 30 | "metadata":{} 31 | }, 32 | "curve_type":"secp256k1", 33 | "currency":{ 34 | "symbol":"THETA", 35 | "decimals":18, 36 | "metadata":{} 37 | } 38 | }, 39 | { 40 | "privkey":"c16a008afc6946a4fb4f62aadf75a0c13f83a8bed29bb178a56f8628fba204eb", 41 | "account_identifier":{ 42 | "address":"0x25302460651DA8D052ed1a405D6f2B6861445e48", 43 | "metadata":{} 44 | }, 45 | "curve_type":"secp256k1", 46 | "currency":{ 47 | "symbol":"TFUEL", 48 | "decimals":18, 49 | "metadata":{} 50 | } 51 | } 52 | ] 53 | }, 54 | "data": { 55 | "start_index": 16281215, 56 | "reconciliation_disabled": false, 57 | "balance_tracking_disabled": false, 58 | "log_blocks": false, 59 | "log_transactions": false, 60 | "log_balance_changes": false, 61 | "log_reconciliations": false, 62 | "end_conditions": { 63 | "reconciliation_coverage": { 64 | "coverage": 0.95, 65 | "from_tip": true, 66 | "tip": true 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export THETA_NETWORK=${THETA_NETWORK:-testnet} 4 | export THETA_MODE=${THETA_MODE:-online} 5 | export THETA_PW=${THETA_PW:-qwertyuiop} 6 | 7 | if [ $THETA_NETWORK == "mainnet" ]; then 8 | echo "Downloading config for ${THETA_NETWORK}" 9 | curl -k --output /app/mainnet/walletnode/config.yaml `curl -k 'https://mainnet-data.thetatoken.org/config?is_guardian=true'` 10 | 11 | echo "config: 12 | path: /app/mainnet/walletnode" >> /app/mainnet/walletnode/config.yaml 13 | echo "data: 14 | path: /data" >> /app/mainnet/walletnode/config.yaml 15 | echo "storage: 16 | statePruningRetainedBlocks: 403200" >> /app/mainnet/walletnode/config.yaml 17 | 18 | MAINNET_SNAPSHOT=/app/mainnet/walletnode/snapshot 19 | 20 | if [ ! -f "$MAINNET_SNAPSHOT" ]; then 21 | echo "Downloading snapshot for ${THETA_NETWORK}" 22 | wget -O /app/mainnet/walletnode/snapshot `curl -k https://mainnet-data.thetatoken.org/snapshot` 23 | fi 24 | 25 | /app/theta start --config=/app/mainnet/walletnode --password=$THETA_PW & 26 | 27 | elif [ $THETA_NETWORK == "testnet" ]; then 28 | mkdir -p /app/testnet/walletnode 29 | cp /app/integration/testnet/walletnode/config.yaml /app/testnet/walletnode/ 30 | 31 | echo "config: 32 | path: /app/testnet/walletnode" >> /app/testnet/walletnode/config.yaml 33 | echo "data: 34 | path: /data" >> /app/testnet/walletnode/config.yaml 35 | echo "storage: 36 | statePruningRetainedBlocks: 403200" >> /app/testnet/walletnode/config.yaml 37 | 38 | TESTNET_SNAPSHOT=/app/testnet/walletnode/snapshot 39 | 40 | if [ ! -f "$TESTNET_SNAPSHOT" ]; then 41 | echo "downloading snapshot for ${THETA_NETWORK}" 42 | wget -O /app/testnet/walletnode/snapshot https://theta-testnet-backup.s3.amazonaws.com/snapshot/snapshot 43 | fi 44 | 45 | /app/theta start --config=/app/testnet/walletnode --password=$THETA_PW & 46 | 47 | else 48 | cp -r /app/integration/privatenet /app/privatenet 49 | mkdir ~/.thetacli 50 | cp -r /app/integration/privatenet/thetacli/* ~/.thetacli/ 51 | chmod 700 ~/.thetacli/keys/encrypted 52 | 53 | echo "config: 54 | path: /app/privatenet/node" >> /app/privatenet/node/config.yaml 55 | echo "data: 56 | path: /data" >> /app/privatenet/node/config.yaml 57 | 58 | /app/theta start --config=/app/privatenet/node --password=$THETA_PW & 59 | fi 60 | 61 | STATUS=`/app/thetacli query status` 62 | 63 | if [[ $STATUS == Failed* ]]; then 64 | echo "waiting for Theta node to finish startup" 65 | fi 66 | 67 | while [[ $STATUS == Failed* ]] 68 | do 69 | sleep 5 70 | STATUS=`/app/thetacli query status` 71 | done 72 | 73 | /app/theta-rosetta-rpc-adaptor start --mode=$THETA_MODE -------------------------------------------------------------------------------- /common/db.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/syndtr/goleveldb/leveldb" 7 | "github.com/syndtr/goleveldb/leveldb/errors" 8 | "github.com/syndtr/goleveldb/leveldb/filter" 9 | "github.com/syndtr/goleveldb/leveldb/iterator" 10 | "github.com/syndtr/goleveldb/leveldb/opt" 11 | ) 12 | 13 | type LDBDatabase struct { 14 | fn string // filename for reporting 15 | db *leveldb.DB // LevelDB instance 16 | 17 | quitLock sync.Mutex // Mutex protecting the quit channel access 18 | } 19 | 20 | // NewLDBDatabase returns a LevelDB wrapped object. 21 | func NewLDBDatabase(file string, cache int, handles int) (*LDBDatabase, error) { 22 | // Ensure we have some minimal caching and file guarantees 23 | if cache < 16 { 24 | cache = 16 25 | } 26 | if handles < 16 { 27 | handles = 16 28 | } 29 | logger.Infof("Allocated cache and file handles, cache: %v, handles: %v", cache, handles) 30 | 31 | // Open the db and recover any potential corruptions 32 | db, err := leveldb.OpenFile(file, &opt.Options{ 33 | OpenFilesCacheCapacity: handles, 34 | BlockCacheCapacity: cache / 2 * opt.MiB, 35 | WriteBuffer: cache / 4 * opt.MiB, // Two of these are used internally 36 | Filter: filter.NewBloomFilter(10), 37 | }) 38 | if _, corrupted := err.(*errors.ErrCorrupted); corrupted { 39 | db, err = leveldb.RecoverFile(file, nil) 40 | } 41 | // (Re)check for errors and abort if opening of the db failed 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | return &LDBDatabase{ 47 | fn: file, 48 | db: db, 49 | }, nil 50 | } 51 | 52 | // Path returns the path to the database directory. 53 | func (db *LDBDatabase) Path() string { 54 | return db.fn 55 | } 56 | 57 | // Put puts the given key / value to the queue 58 | func (db *LDBDatabase) Put(key []byte, value []byte) error { 59 | return db.db.Put(key, value, nil) 60 | } 61 | 62 | func (db *LDBDatabase) Has(key []byte) (bool, error) { 63 | return db.db.Has(key, nil) 64 | } 65 | 66 | // Get returns the given key if it's present. 67 | func (db *LDBDatabase) Get(key []byte) ([]byte, error) { 68 | dat, err := db.db.Get(key, nil) 69 | if err != nil { 70 | if err == leveldb.ErrNotFound { 71 | return nil, errors.New("KeyNotFound") 72 | } 73 | return nil, err 74 | } 75 | return dat, nil 76 | } 77 | 78 | // Delete deletes the key from the queue and database 79 | func (db *LDBDatabase) Delete(key []byte) error { 80 | err := db.db.Delete(key, nil) 81 | if err != nil && err == leveldb.ErrNotFound { 82 | return errors.New("KeyNotFound") 83 | } 84 | return err 85 | } 86 | 87 | func (db *LDBDatabase) Close() { 88 | db.quitLock.Lock() 89 | defer db.quitLock.Unlock() 90 | 91 | err := db.db.Close() 92 | if err == nil { 93 | logger.Infof("Database closed") 94 | } else { 95 | logger.Errorf("Failed to close database, err: %v", err) 96 | } 97 | } 98 | 99 | func (db *LDBDatabase) LDB() *leveldb.DB { 100 | return db.db 101 | } 102 | 103 | func (db *LDBDatabase) NewIterator() iterator.Iterator { 104 | return db.db.NewIterator(nil, nil) 105 | } 106 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # theta-rosetta-rpc-adaptor 2 |

3 | 4 | Rosetta 5 | 6 |

7 |

8 | Theta Rosetta 9 |

10 | 11 | ## Overview 12 | `Theta-rosetta` provides a reference implementation of the [Rosetta specification](https://github.com/coinbase/rosetta-specifications) for Theta. 13 | 14 | ## Features 15 | * Comprehensive tracking of all ETH balance changes 16 | * Stateless, offline, curve-based transaction construction (with address checksum validation) 17 | * Atomic balance lookups using go-ethereum's GraphQL Endpoint 18 | * Idempotent access to all transaction traces and receipts 19 | 20 | ## Pre-requisite 21 | To run `Theta-rosetta`, you must install Docker. Please refer to [Docker official documentation](https://docs.docker.com/get-docker/) on installation instruction. 22 | 23 | ## Usage 24 | As specified in the [Rosetta API Principles](https://www.rosetta-api.org/docs/automated_deployment.html), 25 | all Rosetta implementations must be deployable via Docker and support running via either an 26 | [`online` or `offline` mode](https://www.rosetta-api.org/docs/node_deployment.html#multiple-modes). 27 | 28 | **YOU MUST INSTALL DOCKER FOR THE FOLLOWING INSTRUCTIONS TO WORK. YOU CAN DOWNLOAD 29 | DOCKER [HERE](https://www.docker.com/get-started).** 30 | 31 | ## Install 32 | Running the following commands will create a Docker image called `theta-rosetta-rpc-adaptor:latest`. 33 | 34 | _Get the Dockerfile from this repository and put it into your desired local directory_ 35 | 36 | ```text 37 | docker build --no-cache -t theta-rosetta-rpc-adaptor:latest . 38 | ``` 39 | 40 | ## Run 41 | Running the following command will start a Docker container and expose the Rosetta APIs. 42 | ```shell script 43 | docker run -p 8080:8080 -p 16888:16888 -p 15872:15872 -p 21000:21000 -p 30001:30001 -e THETA_NETWORK=testnet -it theta-rosetta-rpc-adaptor:latest 44 | ``` 45 | 46 | ### Restarting `Theta-rosetta` 47 | 48 | ``` 49 | docker stop 50 | docker start 51 | ``` 52 | 53 | ## Restful APIs 54 | 55 | ### Rosetta restful APIs 56 | 57 | #### All Data APIs specified in https://www.rosetta-api.org/docs/data_api_introduction.html 58 | 59 | #### All supported Construction APIs specified in https://www.rosetta-api.org/docs/ConstructionApi.html 60 | 61 | 62 | ### Unsupported APIs 63 | 64 | Indexer APIs specifed in https://www.rosetta-api.org/docs/indexers.html 65 | 66 | 67 | ## How to test 68 | Install the latest rosetta-cli from https://github.com/coinbase/rosetta-cli. 69 | 70 | ### Testing Data API 71 | 72 | ``` 73 | rosetta-cli check:data --configuration-file=cli-test-config.json 74 | ``` 75 | 76 | ### Testing Construction API 77 | 78 | ``` 79 | rosetta-cli check:construction --configuration-file=cli-test-config.json 80 | ``` 81 | 82 | ### End Conditions 83 | 84 | The end conditions for `check:construction` is set to: 85 | ``` 86 | broadcast complete for job "transfer (3)" with transaction hash 87 | ``` 88 | 89 | 90 | ## License 91 | 92 | This project is available open source under the terms of the [GNU Lesser General Public License 3.0](LICENSE.md). 93 | 94 | © 2021 Theta 95 | -------------------------------------------------------------------------------- /services/server.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | // "fmt" 5 | "errors" 6 | "fmt" 7 | "net/http" 8 | "os" 9 | 10 | log "github.com/sirupsen/logrus" 11 | "github.com/spf13/viper" 12 | 13 | "github.com/coinbase/rosetta-sdk-go/asserter" 14 | "github.com/coinbase/rosetta-sdk-go/server" 15 | "github.com/coinbase/rosetta-sdk-go/types" 16 | 17 | jrpc "github.com/ybbus/jsonrpc" 18 | 19 | cmn "github.com/thetatoken/theta-rosetta-rpc-adaptor/common" 20 | ) 21 | 22 | var logger *log.Entry = log.WithFields(log.Fields{"prefix": "rpc"}) 23 | 24 | func StartServers() error { 25 | client := jrpc.NewClient(cmn.GetThetaRPCEndpoint()) 26 | 27 | router, err := NewThetaRouter(client) 28 | if err != nil { 29 | logger.Fatalf("ERROR: Failed to init router: %v\n", err) 30 | } 31 | 32 | httpAddr := viper.GetString(cmn.CfgRPCHttpAddress) 33 | httpPort := viper.GetString(cmn.CfgRPCHttpPort) 34 | httpEndpoint := fmt.Sprintf("%v:%v", httpAddr, httpPort) 35 | 36 | logger.Infof("Started listening at: %v\n", httpEndpoint) 37 | if err := http.ListenAndServe(httpEndpoint, router); err != nil { 38 | logger.Fatalf("Theta Rosetta Adaptor server exited with error: %v\n", err) 39 | } 40 | 41 | return nil 42 | } 43 | 44 | func StopServers() error { 45 | return nil 46 | } 47 | 48 | // NewThetaRouter returns a Mux http.Handler from a collection of 49 | // Rosetta service controllers. 50 | func NewThetaRouter(client jrpc.RPCClient) (http.Handler, error) { 51 | status, err := cmn.GetStatus(client) 52 | if err != nil { 53 | return nil, err 54 | } 55 | cmn.SetChainId(status.ChainID) 56 | 57 | asserter, err := asserter.NewServer( 58 | cmn.TxOpTypes(), 59 | true, 60 | []*types.NetworkIdentifier{ 61 | { 62 | Blockchain: cmn.ChainName, 63 | Network: status.ChainID, 64 | }, 65 | }, 66 | []string{}, 67 | false, 68 | ) 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | needQueryReturnStakes := false 74 | if _, err := os.Stat("/data/return_stakes"); errors.Is(err, os.ErrNotExist) { 75 | needQueryReturnStakes = true 76 | } 77 | 78 | db, err := cmn.NewLDBDatabase("/data/return_stakes", 64, 0) 79 | stakeService := cmn.NewStakeService(client, db) 80 | 81 | // populate kvstore for vcp/gcp/eenp stakes having withdrawn:true 82 | if needQueryReturnStakes { 83 | stakeService.GenStakesForSnapshot() 84 | } 85 | 86 | // //temp 87 | // iter := db.NewIterator() 88 | // for iter.Next() { 89 | // key := iter.Key() 90 | // returnStakeTxs := cmn.ReturnStakeTxs{} 91 | // kvstore := cmn.NewKVStore(db) 92 | // kvstore.Get(key, &returnStakeTxs) 93 | // } 94 | // iter.Release() 95 | // err = iter.Error() 96 | 97 | networkAPIController := server.NewNetworkAPIController(NewNetworkAPIService(client), asserter) 98 | accountAPIController := server.NewAccountAPIController(NewAccountAPIService(client), asserter) 99 | blockAPIController := server.NewBlockAPIController(NewBlockAPIService(client, db, stakeService), asserter) 100 | memPoolAPIController := server.NewMempoolAPIController(NewMemPoolAPIService(client), asserter) 101 | constructionAPIController := server.NewConstructionAPIController(NewConstructionAPIService(client), asserter) 102 | r := server.NewRouter(networkAPIController, accountAPIController, blockAPIController, memPoolAPIController, constructionAPIController) 103 | return server.CorsMiddleware(server.LoggerMiddleware(r)), nil 104 | } 105 | -------------------------------------------------------------------------------- /services/mempool.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/spf13/viper" 10 | 11 | "github.com/coinbase/rosetta-sdk-go/server" 12 | "github.com/coinbase/rosetta-sdk-go/types" 13 | cmn "github.com/thetatoken/theta-rosetta-rpc-adaptor/common" 14 | jrpc "github.com/ybbus/jsonrpc" 15 | ) 16 | 17 | type GetPendingTransactionsArgs struct { 18 | } 19 | 20 | type GetPendingTransactionsResult struct { 21 | TxHashes []string `json:"tx_hashes"` 22 | } 23 | 24 | type memPoolAPIService struct { 25 | client jrpc.RPCClient 26 | } 27 | 28 | // NewMemPoolAPIService creates a new instance of an MemPoolAPIService. 29 | func NewMemPoolAPIService(client jrpc.RPCClient) server.MempoolAPIServicer { 30 | return &memPoolAPIService{ 31 | client: client, 32 | } 33 | } 34 | 35 | // MemPool implements the /mempool endpoint. 36 | func (s *memPoolAPIService) Mempool( 37 | ctx context.Context, 38 | request *types.NetworkRequest, 39 | ) (*types.MempoolResponse, *types.Error) { 40 | if !strings.EqualFold(cmn.CfgRosettaModeOnline, viper.GetString(cmn.CfgRosettaMode)) { 41 | return nil, cmn.ErrUnavailableOffline 42 | } 43 | 44 | if err := cmn.ValidateNetworkIdentifier(ctx, request.NetworkIdentifier); err != nil { 45 | return nil, err 46 | } 47 | 48 | rpcRes, rpcErr := s.client.Call("theta.GetPendingTransactions", GetPendingTransactionsArgs{}) 49 | 50 | parse := func(jsonBytes []byte) (interface{}, error) { 51 | pendingTxs := GetPendingTransactionsResult{} 52 | json.Unmarshal(jsonBytes, &pendingTxs) 53 | 54 | resp := types.MempoolResponse{} 55 | resp.TransactionIdentifiers = make([]*types.TransactionIdentifier, 0) 56 | for _, txHash := range pendingTxs.TxHashes { 57 | txId := types.TransactionIdentifier{ 58 | Hash: txHash, 59 | } 60 | resp.TransactionIdentifiers = append(resp.TransactionIdentifiers, &txId) 61 | } 62 | 63 | return resp, nil 64 | } 65 | 66 | res, err := cmn.HandleThetaRPCResponse(rpcRes, rpcErr, parse) 67 | if err != nil { 68 | return nil, cmn.ErrUnableToGetMemPool 69 | } 70 | 71 | ret, _ := res.(types.MempoolResponse) 72 | return &ret, nil 73 | } 74 | 75 | // MempoolTransaction implements the /mempool/transaction endpoint. 76 | func (s *memPoolAPIService) MempoolTransaction( 77 | ctx context.Context, 78 | request *types.MempoolTransactionRequest, 79 | ) (*types.MempoolTransactionResponse, *types.Error) { 80 | if err := cmn.ValidateNetworkIdentifier(ctx, request.NetworkIdentifier); err != nil { 81 | return nil, err 82 | } 83 | 84 | rpcRes, rpcErr := s.client.Call("theta.GetTransaction", GetTransactionArgs{ 85 | Hash: request.TransactionIdentifier.Hash, 86 | }) 87 | 88 | parse := func(jsonBytes []byte) (interface{}, error) { 89 | txResult := GetTransactionResult{} 90 | json.Unmarshal(jsonBytes, &txResult) 91 | 92 | var gasUsed uint64 93 | if txResult.Receipt != nil { 94 | gasUsed = txResult.Receipt.GasUsed 95 | } 96 | 97 | resp := types.MempoolTransactionResponse{} 98 | 99 | var objMap map[string]json.RawMessage 100 | json.Unmarshal(jsonBytes, &objMap) 101 | if objMap["transaction"] != nil { 102 | var rawTx json.RawMessage 103 | json.Unmarshal(objMap["transaction"], &rawTx) 104 | status := string(txResult.Status) 105 | if "not_found" != status { 106 | tx := cmn.ParseTx(cmn.TxType(txResult.Type), rawTx, txResult.TxHash, &status, gasUsed, txResult.BalanceChanges, nil, nil, 0) 107 | resp.Transaction = &tx 108 | } 109 | } 110 | if resp.Transaction == nil { 111 | return nil, fmt.Errorf("%v", cmn.ErrUnableToGetBlkTx) 112 | } 113 | return resp, nil 114 | } 115 | 116 | res, err := cmn.HandleThetaRPCResponse(rpcRes, rpcErr, parse) 117 | if err != nil { 118 | return nil, cmn.ErrUnableToGetBlkTx 119 | } 120 | 121 | ret, _ := res.(types.MempoolTransactionResponse) 122 | return &ret, nil 123 | } 124 | -------------------------------------------------------------------------------- /common/types.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/coinbase/rosetta-sdk-go/types" 7 | "github.com/thetatoken/theta/blockchain" 8 | "github.com/thetatoken/theta/common" 9 | ttypes "github.com/thetatoken/theta/ledger/types" 10 | ) 11 | 12 | const ( 13 | ChainName = "theta" 14 | 15 | // Decimals 16 | CoinDecimals = 18 17 | ) 18 | 19 | func GetThetaCurrency() *types.Currency { 20 | return &types.Currency{ 21 | Symbol: "THETA", 22 | Decimals: CoinDecimals, 23 | } 24 | } 25 | 26 | func GetTFuelCurrency() *types.Currency { 27 | return &types.Currency{ 28 | Symbol: "TFUEL", 29 | Decimals: CoinDecimals, 30 | } 31 | } 32 | 33 | // ------------------------------ Tx Type ----------------------------------- 34 | 35 | type TxType byte 36 | type TxStatus string 37 | 38 | type Tx struct { 39 | ttypes.Tx `json:"raw"` 40 | Type TxType `json:"type"` 41 | Hash common.Hash `json:"hash"` 42 | Receipt *blockchain.TxReceiptEntry `json:"receipt"` 43 | BalanceChanges *blockchain.TxBalanceChangesEntry `json:"balance_changes"` 44 | } 45 | 46 | const ( 47 | CoinbaseTx TxType = iota 48 | SlashTx 49 | SendTx 50 | ReserveFundTx 51 | ReleaseFundTx 52 | ServicePaymentTx 53 | SplitRuleTx 54 | SmartContractTx 55 | DepositStakeTx 56 | WithdrawStakeTx 57 | DepositStakeV2Tx 58 | StakeRewardDistributionTx 59 | ) 60 | 61 | func (t TxType) String() string { 62 | return [...]string{ 63 | "CoinbaseTx", 64 | "SlashTx", 65 | "SendTx", 66 | "ReserveFundTx", 67 | "ServicePaymentTx", 68 | "SplitRuleTx", 69 | "SmartContractTx", 70 | "DepositStakeTx", 71 | "WithdrawStakeTx", 72 | "DepositStakeV2Tx", 73 | "StakeRewardDistributionTx", 74 | }[t] 75 | } 76 | 77 | func TxTypes() []string { 78 | return []string{ 79 | "CoinbaseTx", 80 | "SlashTx", 81 | "SendTx", 82 | "ReserveFundTx", 83 | "ServicePaymentTx", 84 | "SplitRuleTx", 85 | "SmartContractTx", 86 | "DepositStakeTx", 87 | "WithdrawStakeTx", 88 | "DepositStakeV2Tx", 89 | "StakeRewardDistributionTx", 90 | } 91 | } 92 | 93 | // ------------------------------ Tx Operation Type ----------------------------------- 94 | 95 | type TxOpType byte 96 | 97 | const ( 98 | CoinbaseTxProposer TxOpType = iota 99 | CoinbaseTxOutput 100 | SlashTxProposer 101 | SendTxInput 102 | SendTxOutput 103 | ReserveFundTxSource 104 | ReleaseFundTxSource 105 | ServicePaymentTxSource 106 | ServicePaymentTxTarget 107 | SplitRuleTxInitiator 108 | SmartContractTxFrom 109 | SmartContractTxTo 110 | DepositStakeTxSource 111 | DepositStakeTxHolder 112 | WithdrawStakeTxSource 113 | WithdrawStakeTxHolder 114 | StakeRewardDistributionTxHolder 115 | StakeRewardDistributionTxBeneficiary 116 | TxFee 117 | ) 118 | 119 | func (t TxOpType) String() string { 120 | return [...]string{ 121 | "CoinbaseTxProposer", 122 | "CoinbaseTxOutput", 123 | "SlashTxProposer", 124 | "SendTxInput", 125 | "SendTxOutput", 126 | "ReserveFundTxSource", 127 | "ReleaseFundTxSource", 128 | "ServicePaymentTxSource", 129 | "ServicePaymentTxTarget", 130 | "SplitRuleTxInitiator", 131 | "SmartContractTxFrom", 132 | "SmartContractTxTo", 133 | "DepositStakeTxSource", 134 | "DepositStakeTxHolder", 135 | "WithdrawStakeTxSource", 136 | "WithdrawStakeTxHolder", 137 | "StakeRewardDistributionTxHolder", 138 | "StakeRewardDistributionTxBeneficiary", 139 | "TxFee", 140 | }[t] 141 | } 142 | 143 | func TxOpTypes() []string { 144 | return []string{ 145 | "CoinbaseTxProposer", 146 | "CoinbaseTxOutput", 147 | "SlashTxProposer", 148 | "SendTxInput", 149 | "SendTxOutput", 150 | "ReserveFundTxSource", 151 | "ReleaseFundTxSource", 152 | "ServicePaymentTxSource", 153 | "ServicePaymentTxTarget", 154 | "SplitRuleTxInitiator", 155 | "SmartContractTxFrom", 156 | "SmartContractTxTo", 157 | "DepositStakeTxSource", 158 | "DepositStakeTxHolder", 159 | "WithdrawStakeTxSource", 160 | "WithdrawStakeTxHolder", 161 | "StakeRewardDistributionTxHolder", 162 | "StakeRewardDistributionTxBeneficiary", 163 | "TxFee", 164 | } 165 | } 166 | 167 | //TODO: merge these two? 168 | func IsSupportedConstructionType(typ string) bool { 169 | for _, styp := range TxOpTypes() { 170 | if typ == styp { 171 | return true 172 | } 173 | } 174 | return false 175 | } 176 | 177 | func GetTxOpType(typ string) (TxOpType, error) { 178 | for i, styp := range TxOpTypes() { 179 | if typ == styp { 180 | return TxOpType(i), nil 181 | } 182 | } 183 | return 0, fmt.Errorf("invalid tx op type") 184 | } 185 | 186 | // ------------------------------ Block Status ----------------------------------- 187 | 188 | type BlockStatus byte 189 | 190 | const ( 191 | BlockStatusPending BlockStatus = iota 192 | BlockStatusValid 193 | BlockStatusInvalid 194 | BlockStatusCommitted 195 | BlockStatusDirectlyFinalized 196 | BlockStatusIndirectlyFinalized 197 | BlockStatusTrusted 198 | BlockStatusDisposed 199 | ) 200 | 201 | func (s BlockStatus) String() string { 202 | return [...]string{ 203 | "pending", 204 | "valid", 205 | "invalid", 206 | "committed", 207 | "directly_finalized", 208 | "indirectly_finalized", 209 | "trusted", 210 | "disposed", 211 | }[s] 212 | } 213 | 214 | // ------------------------------ Withdraw (Return) Stake Txs ----------------------------------- 215 | 216 | type ReturnStakeTx struct { 217 | Hash string `json:"hash"` 218 | Tx ttypes.WithdrawStakeTx `json:"tx"` 219 | } 220 | 221 | type ReturnStakeTxs struct { 222 | ReturnStakes []*ReturnStakeTx `json:"return_stakes"` 223 | } 224 | -------------------------------------------------------------------------------- /services/network.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "strings" 8 | 9 | "github.com/coinbase/rosetta-sdk-go/server" 10 | "github.com/coinbase/rosetta-sdk-go/types" 11 | "github.com/spf13/viper" 12 | 13 | jrpc "github.com/ybbus/jsonrpc" 14 | 15 | cmn "github.com/thetatoken/theta-rosetta-rpc-adaptor/common" 16 | "github.com/thetatoken/theta/version" 17 | ) 18 | 19 | type networkAPIService struct { 20 | client jrpc.RPCClient 21 | } 22 | 23 | // NewAccountAPIService creates a new instance of an AccountAPIService. 24 | func NewNetworkAPIService(client jrpc.RPCClient) server.NetworkAPIServicer { 25 | return &networkAPIService{ 26 | client: client, 27 | } 28 | } 29 | 30 | // NetworkList implements the /network/list endpoint. 31 | func (s *networkAPIService) NetworkList( 32 | ctx context.Context, 33 | request *types.MetadataRequest, 34 | ) (*types.NetworkListResponse, *types.Error) { 35 | return &types.NetworkListResponse{ 36 | NetworkIdentifiers: []*types.NetworkIdentifier{{ 37 | Blockchain: "theta", 38 | Network: cmn.GetChainId(), 39 | }, 40 | }, 41 | }, nil 42 | } 43 | 44 | // NetworkStatus implements the /network/status endpoint. 45 | func (s *networkAPIService) NetworkStatus( 46 | ctx context.Context, 47 | request *types.NetworkRequest, 48 | ) (*types.NetworkStatusResponse, *types.Error) { 49 | if !strings.EqualFold(cmn.CfgRosettaModeOnline, viper.GetString(cmn.CfgRosettaMode)) { 50 | return nil, cmn.ErrUnavailableOffline 51 | } 52 | 53 | if err := cmn.ValidateNetworkIdentifier(ctx, request.NetworkIdentifier); err != nil { 54 | return nil, err 55 | } 56 | 57 | status, err := cmn.GetStatus(s.client) 58 | if err != nil { 59 | return nil, cmn.ErrUnableToGetNodeStatus 60 | } 61 | 62 | lastFinalized := int64(status.LatestFinalizedBlockHeight) 63 | currHeight := int64(status.CurrentHeight) 64 | synced := !status.Syncing 65 | 66 | skipEdgeNode := true 67 | if request.Metadata != nil { 68 | if val, ok := request.Metadata["skip_edge_node"]; ok { 69 | skipEdgeNode = val.(bool) 70 | } 71 | } 72 | 73 | peers, err := GetPeers(s.client, skipEdgeNode) 74 | if err != nil { 75 | return nil, cmn.ErrUnableToGetNodeStatus 76 | } 77 | 78 | peerList := make([]*types.Peer, 0) 79 | for _, peerId := range peers.Peers { 80 | peer := types.Peer{PeerID: peerId} 81 | peerList = append(peerList, &peer) 82 | } 83 | 84 | resp := &types.NetworkStatusResponse{ 85 | CurrentBlockIdentifier: &types.BlockIdentifier{Index: lastFinalized, Hash: status.LatestFinalizedBlockHash.Hex()}, 86 | CurrentBlockTimestamp: status.LatestFinalizedBlockTime.ToInt().Int64() * 1000, 87 | GenesisBlockIdentifier: &types.BlockIdentifier{Index: 0, Hash: status.GenesisBlockHash.Hex()}, 88 | OldestBlockIdentifier: &types.BlockIdentifier{Index: int64(status.SnapshotBlockHeight), Hash: status.SnapshotBlockHash.Hex()}, 89 | SyncStatus: &types.SyncStatus{CurrentIndex: &lastFinalized, TargetIndex: &currHeight, Synced: &synced}, 90 | Peers: peerList, 91 | } 92 | 93 | return resp, nil 94 | } 95 | 96 | // NetworkOptions implements the /network/options endpoint. 97 | func (s *networkAPIService) NetworkOptions( 98 | ctx context.Context, 99 | request *types.NetworkRequest, 100 | ) (*types.NetworkOptionsResponse, *types.Error) { 101 | if err := cmn.ValidateNetworkIdentifier(ctx, request.NetworkIdentifier); err != nil { 102 | return nil, err 103 | } 104 | 105 | return &types.NetworkOptionsResponse{ 106 | Version: &types.Version{ 107 | RosettaVersion: viper.GetString(cmn.CfgRosettaVersion), 108 | NodeVersion: version.Version, 109 | }, 110 | Allow: &types.Allow{ 111 | OperationStatuses: []*types.OperationStatus{ 112 | { 113 | Status: cmn.BlockStatusPending.String(), 114 | Successful: false, 115 | }, 116 | { 117 | Status: cmn.BlockStatusValid.String(), 118 | Successful: true, 119 | }, 120 | { 121 | Status: cmn.BlockStatusInvalid.String(), 122 | Successful: false, 123 | }, 124 | { 125 | Status: cmn.BlockStatusCommitted.String(), 126 | Successful: true, 127 | }, 128 | { 129 | Status: cmn.BlockStatusDirectlyFinalized.String(), 130 | Successful: true, 131 | }, 132 | { 133 | Status: cmn.BlockStatusIndirectlyFinalized.String(), 134 | Successful: true, 135 | }, 136 | { 137 | Status: cmn.BlockStatusTrusted.String(), 138 | Successful: true, 139 | }, 140 | { 141 | Status: cmn.BlockStatusDisposed.String(), 142 | Successful: true, 143 | }, 144 | }, 145 | OperationTypes: cmn.TxOpTypes(), 146 | Errors: cmn.ErrorList, 147 | HistoricalBalanceLookup: true, 148 | MempoolCoins: true, // Any Rosetta implementation that can update an AccountIdentifier's unspent coins based on the 149 | // contents of the mempool should populate this field as true. If false, requests to 150 | // `/account/coins` that set `include_mempool` as true will be automatically rejected 151 | }, 152 | }, nil 153 | } 154 | 155 | type GetPeersArgs struct { 156 | SkipEdgeNode bool `json:"skip_edge_node"` 157 | } 158 | 159 | type GetPeersResult struct { 160 | Peers []string `json:"peers"` 161 | } 162 | 163 | func GetPeers(client jrpc.RPCClient, skipEdgeNode bool) (*GetPeersResult, error) { 164 | rpcRes, rpcErr := client.Call("theta.GetPeers", GetPeersArgs{SkipEdgeNode: skipEdgeNode}) 165 | if rpcErr != nil { 166 | return nil, rpcErr 167 | } 168 | jsonBytes, err := json.MarshalIndent(rpcRes.Result, "", " ") 169 | if err != nil { 170 | return nil, fmt.Errorf("failed to parse theta RPC response: %v, %s", err, string(jsonBytes)) 171 | } 172 | trpcResult := GetPeersResult{} 173 | json.Unmarshal(jsonBytes, &trpcResult) 174 | 175 | return &trpcResult, nil 176 | } 177 | -------------------------------------------------------------------------------- /theta.ros: -------------------------------------------------------------------------------- 1 | request_funds(1){ 2 | find_account{ 3 | currency = {"symbol":"THETA", "decimals":18}; 4 | random_account = find_balance({ 5 | "minimum_balance":{ 6 | "value": "0", 7 | "currency": {{currency}} 8 | }, 9 | "create_limit":1 10 | }); 11 | }, 12 | 13 | // Create a separate scenario to request funds so that 14 | // the address we are using to request funds does not 15 | // get rolled back if funds do not yet exist. 16 | request{ 17 | loaded_account = find_balance({ 18 | "account_identifier": {{random_account.account_identifier}}, 19 | "minimum_balance":{ 20 | "value": "10000000000000000", 21 | "currency": {{currency}} 22 | } 23 | }); 24 | } 25 | } 26 | 27 | create_account(1){ 28 | create{ 29 | network = {"network":"testnet", "blockchain":"theta"}; 30 | key = generate_key({"curve_type": "secp256k1"}); 31 | account = derive({ 32 | "network_identifier": {{network}}, 33 | "public_key": {{key.public_key}} 34 | }); 35 | 36 | // If the account is not saved, the key will be lost! 37 | save_account({ 38 | "account_identifier": {{account.account_identifier}}, 39 | "keypair": {{key}} 40 | }); 41 | } 42 | } 43 | 44 | transfer(1){ 45 | transfer{ 46 | transfer.network = {"network":"testnet", "blockchain":"theta"}; 47 | theta_currency = {"symbol":"THETA", "decimals":18}; 48 | tfuel_currency = {"symbol":"TFUEL", "decimals":18}; 49 | sender = find_balance({ 50 | "minimum_balance":{ 51 | "value": "1000000000000000000", // 1 THETA 52 | "currency": {{theta_currency}} 53 | } 54 | }); 55 | 56 | max_fee = "300000000000000000"; 57 | available_amount = {{sender.balance.value}}; 58 | recipient_amount = random_number({"minimum": "1000000000000000000", "maximum": "10000000000000000000"}); 59 | print_message({"recipient_amount":{{recipient_amount}}}); 60 | 61 | // Find recipient and construct operations 62 | sender_amount = 0 - {{recipient_amount}}; 63 | fee = 0 - {{max_fee}} 64 | recipient = find_balance({ 65 | "not_account_identifier":[{{sender.account_identifier}}], 66 | "minimum_balance":{ 67 | "value": "0", 68 | "currency": {{theta_currency}} 69 | }, 70 | "create_limit": 100, 71 | "create_probability": 50 72 | }); 73 | transfer.confirmation_depth = "1"; 74 | transfer.operations = [ 75 | { 76 | "operation_identifier":{"index":0}, 77 | "type":"SendTxInput", 78 | "account":{{sender.account_identifier}}, 79 | "amount":{ 80 | "value":{{sender_amount}}, 81 | "currency":{{theta_currency}} 82 | } 83 | }, 84 | { 85 | "operation_identifier":{"index":1}, 86 | "related_operations": [{"index": 0}], 87 | "type":"SendTxInput", 88 | "account":{{sender.account_identifier}}, 89 | "amount":{ 90 | "value":"0", 91 | "currency":{{tfuel_currency}} 92 | } 93 | }, 94 | { 95 | "operation_identifier":{"index":2}, 96 | "related_operations": [{"index": 1}], 97 | "type":"SendTxOutput", 98 | "account":{{recipient.account_identifier}}, 99 | "amount":{ 100 | "value":{{recipient_amount}}, 101 | "currency":{{theta_currency}} 102 | } 103 | }, 104 | { 105 | "operation_identifier":{"index":3}, 106 | "related_operations": [{"index": 2}], 107 | "type":"SendTxOutput", 108 | "account":{{recipient.account_identifier}}, 109 | "amount":{ 110 | "value":"0", 111 | "currency":{{tfuel_currency}} 112 | } 113 | }, 114 | { 115 | "operation_identifier":{"index":4}, 116 | "related_operations": [{"index": 3}], 117 | "type":"TxFee", 118 | "account":{{sender.account_identifier}}, 119 | "amount":{ 120 | "value":{{fee}}, 121 | "currency":{{tfuel_currency}} 122 | } 123 | } 124 | ]; 125 | } 126 | } 127 | 128 | smart_contract_transfer(1){ 129 | transfer{ 130 | transfer.network = {"network":"testnet", "blockchain":"theta"}; 131 | theta_currency = {"symbol":"THETA", "decimals":18}; 132 | tfuel_currency = {"symbol":"TFUEL", "decimals":18}; 133 | sender = find_balance({ 134 | "minimum_balance":{ 135 | "value": "1000000000000000000", 136 | "currency": {{tfuel_currency}} 137 | } 138 | }); 139 | 140 | gas_price = "4000000000000"; 141 | gas_limit = "30000"; 142 | available_amount = {{sender.balance.value}}; 143 | recipient_amount = random_number({"minimum": "1000000000000000000", "maximum": "10000000000000000000"}); 144 | print_message({"recipient_amount":{{recipient_amount}}}); 145 | 146 | // Find recipient and construct operations 147 | sender_amount = 0 - {{recipient_amount}}; 148 | recipient = find_balance({ 149 | "not_account_identifier":[{{sender.account_identifier}}], 150 | "minimum_balance":{ 151 | "value": "0", 152 | "currency": {{tfuel_currency}} 153 | }, 154 | "create_limit": 100, 155 | "create_probability": 50 156 | }); 157 | transfer.confirmation_depth = "1"; 158 | transfer.operations = [ 159 | { 160 | "operation_identifier":{"index":0}, 161 | "type":"SmartContractTxFrom", 162 | "account":{{sender.account_identifier}}, 163 | "amount":{ 164 | "value":{{sender_amount}}, 165 | "currency":{{tfuel_currency}} 166 | } 167 | }, 168 | { 169 | "operation_identifier":{"index":1}, 170 | "related_operations": [{"index": 0}], 171 | "type":"SmartContractTxTo", 172 | "account":{{recipient.account_identifier}}, 173 | "amount":{ 174 | "value":{{recipient_amount}}, 175 | "currency":{{tfuel_currency}} 176 | } 177 | } 178 | ]; 179 | } 180 | } 181 | 182 | -------------------------------------------------------------------------------- /common/errors.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import "github.com/coinbase/rosetta-sdk-go/types" 4 | 5 | var ( 6 | ErrUnableToGetChainID = &types.Error{ 7 | Code: 1, 8 | Message: "unable to get chain ID", 9 | Retriable: true, 10 | } 11 | 12 | ErrInvalidBlockchain = &types.Error{ 13 | Code: 2, 14 | Message: "invalid blockchain specified in network identifier", 15 | Retriable: false, 16 | } 17 | 18 | ErrInvalidSubnetwork = &types.Error{ 19 | Code: 3, 20 | Message: "invalid sub-network identifier", 21 | Retriable: false, 22 | } 23 | 24 | ErrInvalidNetwork = &types.Error{ 25 | Code: 4, 26 | Message: "invalid network specified in network identifier", 27 | Retriable: false, 28 | } 29 | 30 | ErrMissingNID = &types.Error{ 31 | Code: 5, 32 | Message: "network identifier is missing", 33 | Retriable: false, 34 | } 35 | 36 | ErrUnableToGetLatestBlk = &types.Error{ 37 | Code: 6, 38 | Message: "unable to get latest block", 39 | Retriable: true, 40 | } 41 | 42 | ErrUnableToGetGenesisBlk = &types.Error{ 43 | Code: 7, 44 | Message: "unable to get genesis block", 45 | Retriable: true, 46 | } 47 | 48 | ErrUnableToGetAccount = &types.Error{ 49 | Code: 8, 50 | Message: "unable to get account", 51 | Retriable: true, 52 | } 53 | 54 | ErrMissingBlockHashOrHeight = &types.Error{ 55 | Code: 9, 56 | Message: "missing block hash or height for querying block", 57 | Retriable: false, 58 | } 59 | 60 | ErrInvalidAccountAddress = &types.Error{ 61 | Code: 10, 62 | Message: "invalid account address", 63 | Retriable: false, 64 | } 65 | 66 | ErrMustSpecifySubAccount = &types.Error{ 67 | Code: 11, 68 | Message: "a valid subaccount must be specified ('general' or 'escrow')", 69 | Retriable: false, 70 | } 71 | 72 | ErrUnableToGetBlk = &types.Error{ 73 | Code: 12, 74 | Message: "unable to get block", 75 | Retriable: false, 76 | } 77 | 78 | ErrNotImplemented = &types.Error{ 79 | Code: 13, 80 | Message: "operation not implemented", 81 | Retriable: false, 82 | } 83 | 84 | ErrUnableToGetTxns = &types.Error{ 85 | Code: 14, 86 | Message: "unable to get transactions", 87 | Retriable: false, 88 | } 89 | 90 | ErrUnableToSubmitTx = &types.Error{ 91 | Code: 15, 92 | Message: "unable to submit transaction", 93 | Retriable: false, 94 | } 95 | 96 | ErrUnableToGetNextNonce = &types.Error{ 97 | Code: 16, 98 | Message: "unable to get next nonce", 99 | Retriable: true, 100 | } 101 | 102 | ErrMalformedValue = &types.Error{ 103 | Code: 17, 104 | Message: "malformed value", 105 | Retriable: false, 106 | } 107 | 108 | ErrUnableToGetNodeStatus = &types.Error{ 109 | Code: 18, 110 | Message: "unable to get node status", 111 | Retriable: true, 112 | } 113 | 114 | ErrInvalidInputParam = &types.Error{ 115 | Code: 19, 116 | Message: "Invalid input param: ", 117 | Retriable: false, 118 | } 119 | 120 | ErrUnsupportedPublicKeyType = &types.Error{ 121 | Code: 20, 122 | Message: "unsupported public key type", 123 | Retriable: false, 124 | } 125 | 126 | ErrUnableToParseTx = &types.Error{ 127 | Code: 21, 128 | Message: "unable to parse transaction", 129 | Retriable: false, 130 | } 131 | 132 | ErrInvalidGasPrice = &types.Error{ 133 | Code: 22, 134 | Message: "invalid gas price", 135 | Retriable: false, 136 | } 137 | 138 | ErrUnmarshal = &types.Error{ 139 | Code: 23, 140 | Message: "proto unmarshal error", 141 | Retriable: false, 142 | } 143 | 144 | ErrConstructionCheck = &types.Error{ 145 | Code: 24, 146 | Message: "operation construction check error: ", 147 | Retriable: true, 148 | } 149 | 150 | ErrServiceInternal = &types.Error{ 151 | Code: 25, 152 | Message: "Internal error: ", 153 | Retriable: false, 154 | } 155 | 156 | ErrExceededFee = &types.Error{ 157 | Code: 26, 158 | Message: "exceeded max fee", 159 | Retriable: true, 160 | } 161 | 162 | ErrUnableToEstimateGas = &types.Error{ 163 | Code: 27, 164 | Message: "unable to estimate gas: ", 165 | Retriable: true, 166 | } 167 | 168 | ErrUnableToGetSuggestGas = &types.Error{ 169 | Code: 28, 170 | Message: "unable to get suggest gas: ", 171 | Retriable: true, 172 | } 173 | 174 | ErrUnableToGetBlkTx = &types.Error{ 175 | Code: 29, 176 | Message: "unable to get block transaction", 177 | Retriable: true, 178 | } 179 | 180 | ErrUnableToGetMemPool = &types.Error{ 181 | Code: 30, 182 | Message: "unable to get mempool", 183 | Retriable: true, 184 | } 185 | 186 | ErrUnableToGetMemPoolTx = &types.Error{ 187 | Code: 31, 188 | Message: "unable to get mempool transaction", 189 | Retriable: true, 190 | } 191 | 192 | ErrUnavailableOffline = &types.Error{ 193 | Code: 32, 194 | Message: "Endpoint unavailable offline", 195 | } 196 | 197 | ErrDBKeyNotFound = &types.Error{ 198 | Code: 33, 199 | Message: "db key not found", 200 | } 201 | 202 | ErrorList = []*types.Error{ 203 | ErrUnableToGetChainID, 204 | ErrInvalidBlockchain, 205 | ErrInvalidSubnetwork, 206 | ErrInvalidNetwork, 207 | ErrMissingNID, 208 | ErrUnableToGetLatestBlk, 209 | ErrUnableToGetGenesisBlk, 210 | ErrUnableToGetAccount, 211 | ErrMissingBlockHashOrHeight, 212 | ErrInvalidAccountAddress, 213 | ErrMustSpecifySubAccount, 214 | ErrUnableToGetBlk, 215 | ErrNotImplemented, 216 | ErrUnableToGetTxns, 217 | ErrUnableToSubmitTx, 218 | ErrUnableToGetNextNonce, 219 | ErrMalformedValue, 220 | ErrUnableToGetNodeStatus, 221 | ErrInvalidInputParam, 222 | ErrUnsupportedPublicKeyType, 223 | ErrUnableToParseTx, 224 | ErrInvalidGasPrice, 225 | ErrUnmarshal, 226 | ErrConstructionCheck, 227 | ErrServiceInternal, 228 | ErrExceededFee, 229 | ErrUnableToEstimateGas, 230 | ErrUnableToGetSuggestGas, 231 | ErrUnableToGetBlkTx, 232 | ErrUnableToGetMemPool, 233 | ErrUnableToGetMemPoolTx, 234 | ErrUnavailableOffline, 235 | ErrDBKeyNotFound, 236 | } 237 | ) 238 | -------------------------------------------------------------------------------- /services/block.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "math/big" 8 | "strings" 9 | 10 | "github.com/spf13/viper" 11 | 12 | "github.com/coinbase/rosetta-sdk-go/server" 13 | "github.com/coinbase/rosetta-sdk-go/types" 14 | jrpc "github.com/ybbus/jsonrpc" 15 | 16 | cmn "github.com/thetatoken/theta-rosetta-rpc-adaptor/common" 17 | 18 | "github.com/thetatoken/theta/blockchain" 19 | "github.com/thetatoken/theta/common" 20 | "github.com/thetatoken/theta/core" 21 | ttypes "github.com/thetatoken/theta/ledger/types" 22 | ) 23 | 24 | type blockAPIService struct { 25 | client jrpc.RPCClient 26 | db *cmn.LDBDatabase 27 | stakeService *cmn.StakeService 28 | } 29 | 30 | type GetBlockArgs struct { 31 | Hash common.Hash `json:"hash"` 32 | } 33 | 34 | type GetBlockByHeightArgs struct { 35 | Height common.JSONUint64 `json:"height"` 36 | } 37 | 38 | type GetBlockResult struct { 39 | *GetBlockResultInner 40 | } 41 | 42 | type GetTransactionArgs struct { 43 | Hash string `json:"hash"` 44 | } 45 | 46 | type GetTransactionResult struct { 47 | BlockHash common.Hash `json:"block_hash"` 48 | BlockHeight common.JSONUint64 `json:"block_height"` 49 | Status cmn.TxStatus `json:"status"` 50 | TxHash common.Hash `json:"hash"` 51 | Type byte `json:"type"` 52 | Tx ttypes.Tx `json:"transaction"` 53 | Receipt *blockchain.TxReceiptEntry `json:"receipt"` 54 | BalanceChanges *blockchain.TxBalanceChangesEntry `json:"blance_changes"` 55 | } 56 | 57 | type GetBlockResultInner struct { 58 | ChainID string `json:"chain_id"` 59 | Epoch common.JSONUint64 `json:"epoch"` 60 | Height common.JSONUint64 `json:"height"` 61 | Parent common.Hash `json:"parent"` 62 | TxHash common.Hash `json:"transactions_hash"` 63 | StateHash common.Hash `json:"state_hash"` 64 | Timestamp *common.JSONBig `json:"timestamp"` 65 | Proposer common.Address `json:"proposer"` 66 | HCC core.CommitCertificate `json:"hcc"` 67 | GuardianVotes *core.AggregatedVotes `json:"guardian_votes"` 68 | EliteEdgeNodeVotes *core.AggregatedEENVotes `json:"elite_edge_node_votes"` 69 | 70 | Children []common.Hash `json:"children"` 71 | Status cmn.BlockStatus `json:"status"` // cmn.BlockStatus has String(). Or core.Status? 72 | 73 | Hash common.Hash `json:"hash"` 74 | Txs []cmn.Tx `json:"transactions"` 75 | } 76 | 77 | // NewBlockAPIService creates a new instance of an AccountAPIService. 78 | func NewBlockAPIService(client jrpc.RPCClient, db *cmn.LDBDatabase, stakeService *cmn.StakeService) server.BlockAPIServicer { 79 | return &blockAPIService{ 80 | client: client, 81 | db: db, 82 | stakeService: stakeService, 83 | } 84 | } 85 | 86 | func (s *blockAPIService) Block( 87 | ctx context.Context, 88 | request *types.BlockRequest, 89 | ) (*types.BlockResponse, *types.Error) { 90 | if !strings.EqualFold(cmn.CfgRosettaModeOnline, viper.GetString(cmn.CfgRosettaMode)) { 91 | return nil, cmn.ErrUnavailableOffline 92 | } 93 | 94 | if err := cmn.ValidateNetworkIdentifier(ctx, request.NetworkIdentifier); err != nil { 95 | return nil, err 96 | } 97 | 98 | var rpcRes *jrpc.RPCResponse 99 | var rpcErr error 100 | 101 | if request.BlockIdentifier.Index != nil { 102 | rpcRes, rpcErr = s.client.Call("theta.GetBlockByHeight", GetBlockByHeightArgs{ 103 | Height: common.JSONUint64(*request.BlockIdentifier.Index), 104 | }) 105 | } else if request.BlockIdentifier.Hash != nil { 106 | rpcRes, rpcErr = s.client.Call("theta.GetBlock", GetBlockArgs{ 107 | Hash: common.HexToHash(*request.BlockIdentifier.Hash), 108 | }) 109 | } 110 | 111 | if rpcErr != nil { 112 | return nil, cmn.ErrUnableToGetBlk 113 | } 114 | 115 | parse := func(jsonBytes []byte) (interface{}, error) { 116 | tblock := GetBlockResult{} 117 | json.Unmarshal(jsonBytes, &tblock) 118 | 119 | if tblock.GetBlockResultInner == nil { 120 | return nil, fmt.Errorf(cmn.ErrUnableToGetBlk.Message) 121 | } 122 | 123 | block := types.Block{} 124 | block.BlockIdentifier = &types.BlockIdentifier{Index: int64(tblock.Height), Hash: tblock.Hash.Hex()} 125 | var parentHeight int64 126 | if tblock.Height == 0 { 127 | parentHeight = 0 128 | } else { 129 | parentHeight = int64(tblock.Height - 1) 130 | } 131 | block.ParentBlockIdentifier = &types.BlockIdentifier{Index: parentHeight, Hash: tblock.Parent.Hex()} 132 | block.Timestamp = tblock.Timestamp.ToInt().Int64() * 1000 133 | block.Metadata = map[string]interface{}{ 134 | "status": tblock.Status.String(), 135 | "transactions_hash": tblock.TxHash, 136 | "state_hash": tblock.StateHash.Hex(), 137 | "proposer": tblock.Proposer.Hex(), 138 | } //TODO: anything else needed? 139 | 140 | txs := make([]*types.Transaction, 0) 141 | status := tblock.Status.String() 142 | var objMap map[string]json.RawMessage 143 | json.Unmarshal(jsonBytes, &objMap) 144 | if objMap["transactions"] != nil { 145 | var txMaps []map[string]json.RawMessage 146 | json.Unmarshal(objMap["transactions"], &txMaps) 147 | for i, txMap := range txMaps { 148 | var gasUsed uint64 149 | if tblock.Txs[i].Receipt != nil { 150 | gasUsed = tblock.Txs[i].Receipt.GasUsed 151 | } 152 | 153 | tx := cmn.ParseTx(tblock.Txs[i].Type, txMap["raw"], tblock.Txs[i].Hash, &status, gasUsed, tblock.Txs[i].BalanceChanges, s.db, s.stakeService, tblock.Height) 154 | txs = append(txs, &tx) 155 | } 156 | } 157 | 158 | // check if there's any stakes that need to be returned at this height 159 | returnStakeTxs := cmn.ReturnStakeTxs{} 160 | kvstore := cmn.NewKVStore(s.db) 161 | if kvstore.Get(new(big.Int).SetUint64(uint64(tblock.Height)).Bytes(), &returnStakeTxs) == nil { 162 | for _, tx := range returnStakeTxs.ReturnStakes { 163 | transaction := types.Transaction{ 164 | TransactionIdentifier: &types.TransactionIdentifier{Hash: tx.Hash}, 165 | } 166 | transaction.Metadata, transaction.Operations = cmn.ParseReturnStakeTx(tx.Tx, &status, cmn.WithdrawStakeTx) 167 | txs = append(txs, &transaction) 168 | } 169 | } 170 | 171 | block.Transactions = txs 172 | 173 | resp := types.BlockResponse{ 174 | Block: &block, 175 | } 176 | return resp, nil 177 | } 178 | 179 | res, err := cmn.HandleThetaRPCResponse(rpcRes, rpcErr, parse) 180 | if err != nil { 181 | return nil, cmn.ErrUnableToGetBlk 182 | } 183 | 184 | ret, _ := res.(types.BlockResponse) 185 | return &ret, nil 186 | } 187 | 188 | // BlockTransaction implements the /block/transaction endpoint. 189 | func (s *blockAPIService) BlockTransaction( 190 | ctx context.Context, 191 | request *types.BlockTransactionRequest, 192 | ) (*types.BlockTransactionResponse, *types.Error) { 193 | if !strings.EqualFold(cmn.CfgRosettaModeOnline, viper.GetString(cmn.CfgRosettaMode)) { 194 | return nil, cmn.ErrUnavailableOffline 195 | } 196 | 197 | if err := cmn.ValidateNetworkIdentifier(ctx, request.NetworkIdentifier); err != nil { 198 | return nil, err 199 | } 200 | 201 | rpcRes, rpcErr := s.client.Call("theta.GetTransaction", GetTransactionArgs{ 202 | Hash: request.TransactionIdentifier.Hash, 203 | }) 204 | 205 | parse := func(jsonBytes []byte) (interface{}, error) { 206 | txResult := GetTransactionResult{} 207 | json.Unmarshal(jsonBytes, &txResult) 208 | 209 | resp := types.BlockTransactionResponse{} 210 | 211 | var objMap map[string]json.RawMessage 212 | json.Unmarshal(jsonBytes, &objMap) 213 | if objMap["transaction"] != nil { 214 | var rawTx json.RawMessage 215 | json.Unmarshal(objMap["transaction"], &rawTx) 216 | 217 | var gasUsed uint64 218 | if txResult.Receipt != nil { 219 | gasUsed = txResult.Receipt.GasUsed 220 | } 221 | 222 | status := string(txResult.Status) 223 | if "not_found" != status { 224 | tx := cmn.ParseTx(cmn.TxType(txResult.Type), rawTx, txResult.TxHash, &status, gasUsed, txResult.BalanceChanges, nil, nil, 0) 225 | resp.Transaction = &tx 226 | } 227 | } 228 | if resp.Transaction == nil { 229 | return nil, fmt.Errorf("%v", cmn.ErrUnableToGetBlkTx) 230 | } 231 | return resp, nil 232 | } 233 | 234 | res, err := cmn.HandleThetaRPCResponse(rpcRes, rpcErr, parse) 235 | if err != nil { 236 | return nil, cmn.ErrUnableToGetBlkTx 237 | } 238 | 239 | ret, _ := res.(types.BlockTransactionResponse) 240 | return &ret, nil 241 | } 242 | -------------------------------------------------------------------------------- /services/account.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "strings" 7 | 8 | "github.com/coinbase/rosetta-sdk-go/server" 9 | "github.com/coinbase/rosetta-sdk-go/types" 10 | 11 | "github.com/spf13/viper" 12 | jrpc "github.com/ybbus/jsonrpc" 13 | 14 | cmn "github.com/thetatoken/theta-rosetta-rpc-adaptor/common" 15 | "github.com/thetatoken/theta/common" 16 | ttypes "github.com/thetatoken/theta/ledger/types" 17 | ) 18 | 19 | // var logger *log.Entry = log.WithFields(log.Fields{"prefix": "account"}) 20 | 21 | type GetAccountArgs struct { 22 | Address string `json:"address"` 23 | Height common.JSONUint64 `json:"height"` 24 | Preview bool `json:"preview"` // preview the account balance from the ScreenedView 25 | } 26 | 27 | type GetAccountResult struct { 28 | *ttypes.Account 29 | Address string `json:"address"` 30 | } 31 | 32 | type accountAPIService struct { 33 | client jrpc.RPCClient 34 | } 35 | 36 | // NewAccountAPIService creates a new instance of an AccountAPIService. 37 | func NewAccountAPIService(client jrpc.RPCClient) server.AccountAPIServicer { 38 | return &accountAPIService{ 39 | client: client, 40 | } 41 | } 42 | 43 | // AccountBalance implements the /account/balance endpoint. 44 | func (s *accountAPIService) AccountBalance( 45 | ctx context.Context, 46 | request *types.AccountBalanceRequest, 47 | ) (*types.AccountBalanceResponse, *types.Error) { 48 | if !strings.EqualFold(cmn.CfgRosettaModeOnline, viper.GetString(cmn.CfgRosettaMode)) { 49 | return nil, cmn.ErrUnavailableOffline 50 | } 51 | 52 | if err := cmn.ValidateNetworkIdentifier(ctx, request.NetworkIdentifier); err != nil { 53 | return nil, err 54 | } 55 | 56 | var blockHeight common.JSONUint64 57 | var blockHash string 58 | if request.BlockIdentifier == nil { 59 | status, err := cmn.GetStatus(s.client) 60 | if err != nil { 61 | return nil, cmn.ErrUnableToGetAccount 62 | } 63 | blockHeight = status.LatestFinalizedBlockHeight 64 | blockHash = status.LatestFinalizedBlockHash.String() 65 | } else { 66 | if request.BlockIdentifier.Index != nil && request.BlockIdentifier.Hash != nil { 67 | blockHeight = common.JSONUint64(*request.BlockIdentifier.Index) 68 | blockHash = *request.BlockIdentifier.Hash 69 | } else if request.BlockIdentifier.Index != nil { 70 | blockHeight = common.JSONUint64(*request.BlockIdentifier.Index) 71 | blk, err := cmn.GetBlockIdentifierByHeight(s.client, blockHeight) 72 | if err != nil { 73 | return nil, err 74 | } 75 | blockHash = blk.Hash.Hex() 76 | } else { 77 | blockHash = *request.BlockIdentifier.Hash 78 | blk, err := cmn.GetBlockIdentifierByHash(s.client, *request.BlockIdentifier.Hash) 79 | if err != nil { 80 | return nil, err 81 | } 82 | blockHeight = blk.Height 83 | } 84 | } 85 | 86 | rpcRes, rpcErr := s.client.Call("theta.GetAccount", GetAccountArgs{ 87 | Address: request.AccountIdentifier.Address, 88 | Height: blockHeight, 89 | }) 90 | 91 | if rpcRes != nil && rpcRes.Error != nil && rpcRes.Error.Code == -32000 { 92 | resp := types.AccountBalanceResponse{} 93 | resp.BlockIdentifier = &types.BlockIdentifier{Index: int64(blockHeight), Hash: blockHash} 94 | 95 | var thetaBalance types.Amount 96 | thetaBalance.Value = "0" 97 | thetaBalance.Currency = cmn.GetThetaCurrency() 98 | resp.Balances = append(resp.Balances, &thetaBalance) 99 | var tfuelBalance types.Amount 100 | tfuelBalance.Value = "0" 101 | tfuelBalance.Currency = cmn.GetTFuelCurrency() 102 | resp.Balances = append(resp.Balances, &tfuelBalance) 103 | 104 | return &resp, nil 105 | } 106 | 107 | parse := func(jsonBytes []byte) (interface{}, error) { 108 | account := GetAccountResult{}.Account 109 | err := json.Unmarshal(jsonBytes, &account) 110 | if err != nil { 111 | return nil, err 112 | } 113 | 114 | resp := types.AccountBalanceResponse{} 115 | resp.BlockIdentifier = &types.BlockIdentifier{Index: int64(blockHeight), Hash: blockHash} 116 | resp.Metadata = map[string]interface{}{"sequence_number": account.Sequence} 117 | 118 | var needTheta, needTFuel bool 119 | if request.Currencies != nil { 120 | for _, currency := range request.Currencies { 121 | if strings.EqualFold(currency.Symbol, cmn.GetThetaCurrency().Symbol) { 122 | needTheta = true 123 | } else if strings.EqualFold(currency.Symbol, cmn.GetTFuelCurrency().Symbol) { 124 | needTFuel = true 125 | } 126 | } 127 | } else { 128 | needTheta = true 129 | needTFuel = true 130 | } 131 | 132 | if needTheta { 133 | var thetaBalance types.Amount 134 | thetaBalance.Value = account.Balance.ThetaWei.String() 135 | thetaBalance.Currency = cmn.GetThetaCurrency() 136 | resp.Balances = append(resp.Balances, &thetaBalance) 137 | } 138 | 139 | if needTFuel { 140 | var tfuelBalance types.Amount 141 | tfuelBalance.Value = account.Balance.TFuelWei.String() 142 | tfuelBalance.Currency = cmn.GetTFuelCurrency() 143 | resp.Balances = append(resp.Balances, &tfuelBalance) 144 | } 145 | 146 | return resp, nil 147 | } 148 | 149 | res, err := cmn.HandleThetaRPCResponse(rpcRes, rpcErr, parse) 150 | if err != nil { 151 | return nil, cmn.ErrUnableToGetAccount 152 | } 153 | 154 | ret, _ := res.(types.AccountBalanceResponse) 155 | return &ret, nil 156 | } 157 | 158 | // AccountCoins implements the /account/coins endpoint. 159 | func (s *accountAPIService) AccountCoins( 160 | ctx context.Context, 161 | request *types.AccountCoinsRequest, 162 | ) (*types.AccountCoinsResponse, *types.Error) { 163 | if !strings.EqualFold(cmn.CfgRosettaModeOnline, viper.GetString(cmn.CfgRosettaMode)) { 164 | return nil, cmn.ErrUnavailableOffline 165 | } 166 | 167 | if err := cmn.ValidateNetworkIdentifier(ctx, request.NetworkIdentifier); err != nil { 168 | return nil, err 169 | } 170 | 171 | status, err := cmn.GetStatus(s.client) 172 | blockIdentifier := &types.BlockIdentifier{Index: int64(status.LatestFinalizedBlockHeight), Hash: status.LatestFinalizedBlockHash.String()} 173 | 174 | rpcRes, rpcErr := s.client.Call("theta.GetAccount", GetAccountArgs{ 175 | Address: request.AccountIdentifier.Address, 176 | }) 177 | 178 | if rpcRes != nil && rpcRes.Error != nil && rpcRes.Error.Code == -32000 { 179 | resp := types.AccountCoinsResponse{} 180 | resp.BlockIdentifier = blockIdentifier 181 | 182 | var thetaCoin types.Coin 183 | thetaCoin.CoinIdentifier = &types.CoinIdentifier{Identifier: cmn.GetThetaCurrency().Symbol} 184 | var thetaBalance types.Amount 185 | thetaBalance.Value = "0" 186 | thetaBalance.Currency = cmn.GetThetaCurrency() 187 | thetaCoin.Amount = &thetaBalance 188 | resp.Coins = append(resp.Coins, &thetaCoin) 189 | 190 | var tfuelCoin types.Coin 191 | tfuelCoin.CoinIdentifier = &types.CoinIdentifier{Identifier: cmn.GetTFuelCurrency().Symbol} 192 | var tfuelBalance types.Amount 193 | tfuelBalance.Value = "0" 194 | tfuelBalance.Currency = cmn.GetTFuelCurrency() 195 | tfuelCoin.Amount = &tfuelBalance 196 | resp.Coins = append(resp.Coins, &tfuelCoin) 197 | 198 | return &resp, nil 199 | } 200 | 201 | parse := func(jsonBytes []byte) (interface{}, error) { 202 | account := GetAccountResult{}.Account 203 | json.Unmarshal(jsonBytes, &account) 204 | 205 | resp := types.AccountCoinsResponse{} 206 | resp.BlockIdentifier = blockIdentifier 207 | resp.Metadata = map[string]interface{}{"sequence_number": account.Sequence} 208 | 209 | var needTheta, needTFuel bool 210 | if request.Currencies != nil { 211 | for _, currency := range request.Currencies { 212 | if strings.EqualFold(currency.Symbol, cmn.GetThetaCurrency().Symbol) { 213 | needTheta = true 214 | } else if strings.EqualFold(currency.Symbol, cmn.GetTFuelCurrency().Symbol) { 215 | needTFuel = true 216 | } 217 | } 218 | } else { 219 | needTheta = true 220 | needTFuel = true 221 | } 222 | 223 | if needTheta { 224 | var thetaCoin types.Coin 225 | thetaCoin.CoinIdentifier = &types.CoinIdentifier{Identifier: cmn.GetThetaCurrency().Symbol} 226 | var thetaBalance types.Amount 227 | thetaBalance.Value = account.Balance.ThetaWei.String() 228 | thetaBalance.Currency = cmn.GetThetaCurrency() 229 | thetaCoin.Amount = &thetaBalance 230 | resp.Coins = append(resp.Coins, &thetaCoin) 231 | } 232 | 233 | if needTFuel { 234 | var tfuelCoin types.Coin 235 | tfuelCoin.CoinIdentifier = &types.CoinIdentifier{Identifier: cmn.GetTFuelCurrency().Symbol} 236 | var tfuelBalance types.Amount 237 | tfuelBalance.Value = account.Balance.TFuelWei.String() 238 | tfuelBalance.Currency = cmn.GetTFuelCurrency() 239 | tfuelCoin.Amount = &tfuelBalance 240 | resp.Coins = append(resp.Coins, &tfuelCoin) 241 | } 242 | 243 | return resp, nil 244 | } 245 | 246 | res, err := cmn.HandleThetaRPCResponse(rpcRes, rpcErr, parse) 247 | if err != nil { 248 | return nil, cmn.ErrUnableToGetAccount 249 | } 250 | 251 | ret, _ := res.(types.AccountCoinsResponse) 252 | return &ret, nil 253 | } 254 | -------------------------------------------------------------------------------- /common/stake_service.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "math/big" 7 | 8 | cmn "github.com/thetatoken/theta/common" 9 | "github.com/thetatoken/theta/core" 10 | "github.com/thetatoken/theta/crypto" 11 | ttypes "github.com/thetatoken/theta/ledger/types" 12 | jrpc "github.com/ybbus/jsonrpc" 13 | ) 14 | 15 | type GetStakeByHeightArgs struct { 16 | Height cmn.JSONUint64 `json:"height"` 17 | } 18 | 19 | type GetVcpResult struct { 20 | BlockHashVcpPairs []BlockHashVcpPair 21 | } 22 | 23 | type BlockHashVcpPair struct { 24 | BlockHash cmn.Hash 25 | Vcp *core.ValidatorCandidatePool 26 | HeightList *ttypes.HeightList 27 | } 28 | 29 | type GetGcpResult struct { 30 | BlockHashGcpPairs []BlockHashGcpPair 31 | } 32 | 33 | type BlockHashGcpPair struct { 34 | BlockHash cmn.Hash 35 | Gcp *core.GuardianCandidatePool 36 | } 37 | 38 | type GetEenpResult struct { 39 | BlockHashEenpPairs []BlockHashEenpPair 40 | } 41 | 42 | type BlockHashEenpPair struct { 43 | BlockHash cmn.Hash 44 | EENs []*core.EliteEdgeNode 45 | } 46 | 47 | type GetEenpStakeByHeightArgs struct { 48 | Height cmn.JSONUint64 `json:"height"` 49 | Source cmn.Address `json:"source"` 50 | Holder cmn.Address `json:"holder"` 51 | WithdrawnOnly bool `json:"withdrawn_only"` 52 | } 53 | 54 | type GetEenpStakeResult struct { 55 | Stake core.Stake `json:"stake"` 56 | } 57 | 58 | type StakeService struct { 59 | client jrpc.RPCClient 60 | db *LDBDatabase 61 | } 62 | 63 | const StakeReturnPrefix = "stake_return" 64 | 65 | func NewStakeService(client jrpc.RPCClient, db *LDBDatabase) *StakeService { 66 | return &StakeService{ 67 | client: client, 68 | db: db, 69 | } 70 | } 71 | 72 | func (ss *StakeService) GenStakesForSnapshot() error { 73 | returnStakeTxsMap := make(map[uint64]ReturnStakeTxs) 74 | 75 | status, err := GetStatus(ss.client) 76 | if err != nil { 77 | return nil 78 | } 79 | snapshotHeight := status.SnapshotBlockHeight 80 | 81 | // VCP 82 | rpcRes, rpcErr := ss.client.Call("theta.GetVcpByHeight", GetStakeByHeightArgs{ 83 | Height: snapshotHeight, 84 | }) 85 | 86 | if rpcErr != nil { 87 | return rpcErr 88 | } 89 | if rpcRes != nil && rpcRes.Error != nil { 90 | return rpcRes.Error 91 | } 92 | 93 | jsonBytes, err := json.MarshalIndent(rpcRes.Result, "", " ") 94 | if err != nil { 95 | return fmt.Errorf("failed to parse theta RPC response: %v, %s", err, string(jsonBytes)) 96 | } 97 | 98 | vcpResult := GetVcpResult{} 99 | json.Unmarshal(jsonBytes, &vcpResult) 100 | if len(vcpResult.BlockHashVcpPairs) > 0 { 101 | for _, candidate := range vcpResult.BlockHashVcpPairs[0].Vcp.SortedCandidates { 102 | blockHash := vcpResult.BlockHashVcpPairs[0].BlockHash.Hex() 103 | for i, stake := range candidate.Stakes { 104 | if stake.Withdrawn { 105 | withdrawStakeTx := ttypes.WithdrawStakeTx{ 106 | Source: ttypes.TxInput{ 107 | Address: stake.Source, 108 | Coins: ttypes.Coins{ThetaWei: stake.Amount, TFuelWei: big.NewInt(0)}, 109 | }, 110 | Holder: ttypes.TxOutput{ 111 | Address: stake.Holder, 112 | }, 113 | } 114 | hash := crypto.Keccak256Hash([]byte(fmt.Sprintf("vcp_%s_%s_%d", StakeReturnPrefix, blockHash, i))) 115 | returnStakeTx := &ReturnStakeTx{ 116 | Hash: hash.Hex(), 117 | Tx: withdrawStakeTx, 118 | } 119 | if returnStakeTxs, ok := returnStakeTxsMap[stake.ReturnHeight]; ok { 120 | returnStakeTxs.ReturnStakes = append(returnStakeTxs.ReturnStakes, returnStakeTx) 121 | returnStakeTxsMap[stake.ReturnHeight] = returnStakeTxs 122 | } else { 123 | returnStakeTxsMap[stake.ReturnHeight] = ReturnStakeTxs{[]*ReturnStakeTx{returnStakeTx}} 124 | } 125 | } 126 | } 127 | } 128 | } 129 | 130 | // GCP 131 | rpcRes, rpcErr = ss.client.Call("theta.GetGcpByHeight", GetStakeByHeightArgs{ 132 | Height: snapshotHeight, 133 | }) 134 | 135 | if rpcErr != nil { 136 | return rpcErr 137 | } 138 | if rpcRes != nil && rpcRes.Error != nil { 139 | return rpcRes.Error 140 | } 141 | 142 | jsonBytes, err = json.MarshalIndent(rpcRes.Result, "", " ") 143 | if err != nil { 144 | return fmt.Errorf("failed to parse theta RPC response: %v, %s", err, string(jsonBytes)) 145 | } 146 | 147 | gcpResult := GetGcpResult{} 148 | json.Unmarshal(jsonBytes, &gcpResult) 149 | if len(gcpResult.BlockHashGcpPairs) > 0 { 150 | for _, guardian := range gcpResult.BlockHashGcpPairs[0].Gcp.SortedGuardians { 151 | blockHash := gcpResult.BlockHashGcpPairs[0].BlockHash.Hex() 152 | for i, stake := range guardian.Stakes { 153 | if stake.Withdrawn { 154 | withdrawStakeTx := ttypes.WithdrawStakeTx{ 155 | Source: ttypes.TxInput{ 156 | Address: stake.Source, 157 | Coins: ttypes.Coins{ThetaWei: stake.Amount, TFuelWei: big.NewInt(0)}, 158 | }, 159 | Holder: ttypes.TxOutput{ 160 | Address: stake.Holder, 161 | }, 162 | } 163 | hash := crypto.Keccak256Hash([]byte(fmt.Sprintf("gcp_%s_%s_%d", StakeReturnPrefix, blockHash, i))) 164 | returnStakeTx := &ReturnStakeTx{ 165 | Hash: hash.Hex(), 166 | Tx: withdrawStakeTx, 167 | } 168 | if returnStakeTxs, ok := returnStakeTxsMap[stake.ReturnHeight]; ok { 169 | returnStakeTxs.ReturnStakes = append(returnStakeTxs.ReturnStakes, returnStakeTx) 170 | } else { 171 | returnStakeTxsMap[stake.ReturnHeight] = ReturnStakeTxs{[]*ReturnStakeTx{returnStakeTx}} 172 | } 173 | } 174 | } 175 | } 176 | } 177 | 178 | // EENP 179 | rpcRes, rpcErr = ss.client.Call("theta.GetEenpByHeight", GetStakeByHeightArgs{ 180 | Height: snapshotHeight, 181 | }) 182 | 183 | if rpcErr != nil { 184 | return rpcErr 185 | } 186 | if rpcRes != nil && rpcRes.Error != nil { 187 | return rpcRes.Error 188 | } 189 | 190 | jsonBytes, err = json.MarshalIndent(rpcRes.Result, "", " ") 191 | if err != nil { 192 | return fmt.Errorf("failed to parse theta RPC response: %v, %s", err, string(jsonBytes)) 193 | } 194 | 195 | eenpResult := GetEenpResult{} 196 | json.Unmarshal(jsonBytes, &eenpResult) 197 | if len(eenpResult.BlockHashEenpPairs) > 0 { 198 | for _, een := range eenpResult.BlockHashEenpPairs[0].EENs { 199 | blockHash := eenpResult.BlockHashEenpPairs[0].BlockHash.Hex() 200 | for i, stake := range een.Stakes { 201 | if stake.Withdrawn { 202 | withdrawStakeTx := ttypes.WithdrawStakeTx{ 203 | Source: ttypes.TxInput{ 204 | Address: stake.Source, 205 | Coins: ttypes.Coins{ThetaWei: big.NewInt(0), TFuelWei: stake.Amount}, 206 | }, 207 | Holder: ttypes.TxOutput{ 208 | Address: stake.Holder, 209 | }, 210 | } 211 | hash := crypto.Keccak256Hash([]byte(fmt.Sprintf("eenp_%s_%s_%d", StakeReturnPrefix, blockHash, i))) 212 | returnStakeTx := &ReturnStakeTx{ 213 | Hash: hash.Hex(), 214 | Tx: withdrawStakeTx, 215 | } 216 | if returnStakeTxs, ok := returnStakeTxsMap[stake.ReturnHeight]; ok { 217 | returnStakeTxs.ReturnStakes = append(returnStakeTxs.ReturnStakes, returnStakeTx) 218 | } else { 219 | returnStakeTxsMap[stake.ReturnHeight] = ReturnStakeTxs{[]*ReturnStakeTx{returnStakeTx}} 220 | } 221 | } 222 | } 223 | } 224 | } 225 | 226 | // store in db 227 | kvstore := NewKVStore(ss.db) 228 | for height, returnStakeTxs := range returnStakeTxsMap { 229 | heightBytes := new(big.Int).SetUint64(uint64(height + 1)).Bytes() // actual return height is off-by-one 230 | kvstore.Put(heightBytes, returnStakeTxs) 231 | } 232 | 233 | return nil 234 | } 235 | 236 | func (ss *StakeService) GetStakeForTx(withdrawStakeTx ttypes.WithdrawStakeTx, blockHeight cmn.JSONUint64) (*core.Stake, error) { 237 | var args interface{} 238 | rpcMethod := "theta." 239 | switch withdrawStakeTx.Purpose { 240 | case core.StakeForValidator: 241 | rpcMethod += "GetVcpByHeight" 242 | args = GetStakeByHeightArgs{Height: blockHeight} 243 | case core.StakeForGuardian: 244 | rpcMethod += "GetGcpByHeight" 245 | args = GetStakeByHeightArgs{Height: blockHeight} 246 | case core.StakeForEliteEdgeNode: 247 | rpcMethod += "GetEenpStakeByHeight" 248 | args = GetEenpStakeByHeightArgs{ 249 | Height: blockHeight, 250 | Source: withdrawStakeTx.Source.Address, 251 | Holder: withdrawStakeTx.Holder.Address, 252 | WithdrawnOnly: true, 253 | } 254 | } 255 | 256 | rpcRes, rpcErr := ss.client.Call(rpcMethod, args) 257 | 258 | if rpcErr != nil { 259 | return nil, rpcErr 260 | } 261 | if rpcRes != nil && rpcRes.Error != nil { 262 | return nil, rpcRes.Error 263 | } 264 | 265 | jsonBytes, err := json.MarshalIndent(rpcRes.Result, "", " ") 266 | if err != nil { 267 | return nil, fmt.Errorf("failed to parse theta RPC response: %v, %s", err, string(jsonBytes)) 268 | } 269 | 270 | switch withdrawStakeTx.Purpose { 271 | case core.StakeForValidator: 272 | vcpResult := GetVcpResult{} 273 | json.Unmarshal(jsonBytes, &vcpResult) 274 | if len(vcpResult.BlockHashVcpPairs) > 0 { 275 | for _, candidate := range vcpResult.BlockHashVcpPairs[0].Vcp.SortedCandidates { 276 | if candidate.Holder == withdrawStakeTx.Holder.Address { 277 | for _, stake := range candidate.Stakes { 278 | if stake.Source == withdrawStakeTx.Source.Address { 279 | return stake, nil 280 | } 281 | } 282 | } 283 | } 284 | } 285 | case core.StakeForGuardian: 286 | gcpResult := GetGcpResult{} 287 | json.Unmarshal(jsonBytes, &gcpResult) 288 | if len(gcpResult.BlockHashGcpPairs) > 0 { 289 | for _, guardian := range gcpResult.BlockHashGcpPairs[0].Gcp.SortedGuardians { 290 | if guardian.Holder == withdrawStakeTx.Holder.Address { 291 | for _, stake := range guardian.Stakes { 292 | if stake.Source == withdrawStakeTx.Source.Address { 293 | return stake, nil 294 | } 295 | } 296 | } 297 | } 298 | } 299 | case core.StakeForEliteEdgeNode: 300 | eenpStakeResult := GetEenpStakeResult{} 301 | json.Unmarshal(jsonBytes, &eenpStakeResult) 302 | return &eenpStakeResult.Stake, nil 303 | } 304 | return nil, nil 305 | } 306 | -------------------------------------------------------------------------------- /services/construction.go: -------------------------------------------------------------------------------- 1 | package services 2 | 3 | import ( 4 | "context" 5 | "crypto/ecdsa" 6 | "crypto/elliptic" 7 | "encoding/hex" 8 | "encoding/json" 9 | "fmt" 10 | "math/big" 11 | "strings" 12 | 13 | "github.com/spf13/viper" 14 | 15 | "github.com/coinbase/rosetta-sdk-go/parser" 16 | "github.com/coinbase/rosetta-sdk-go/server" 17 | "github.com/coinbase/rosetta-sdk-go/types" 18 | 19 | cmn "github.com/thetatoken/theta-rosetta-rpc-adaptor/common" 20 | 21 | "github.com/thetatoken/theta/common" 22 | "github.com/thetatoken/theta/crypto" 23 | "github.com/thetatoken/theta/crypto/secp256k1" 24 | "github.com/thetatoken/theta/crypto/sha3" 25 | ttypes "github.com/thetatoken/theta/ledger/types" 26 | 27 | jrpc "github.com/ybbus/jsonrpc" 28 | ) 29 | 30 | const ( 31 | CurveType = "secp256k1" 32 | SignatureType = "ecdsa_recovery" 33 | ) 34 | 35 | type BroadcastRawTransactionAsyncArgs struct { 36 | TxBytes string `json:"tx_bytes"` 37 | } 38 | 39 | type BroadcastRawTransactionAsyncResult struct { 40 | TxHash string `json:"hash"` 41 | } 42 | 43 | type constructionAPIService struct { 44 | client jrpc.RPCClient 45 | } 46 | 47 | // NewConstructionAPIService creates a new instance of an ConstructionAPIService. 48 | func NewConstructionAPIService(client jrpc.RPCClient) server.ConstructionAPIServicer { 49 | return &constructionAPIService{ 50 | client: client, 51 | } 52 | } 53 | 54 | // ConstructionDerive implements the /construction/derive endpoint. 55 | func (s *constructionAPIService) ConstructionDerive( 56 | ctx context.Context, 57 | request *types.ConstructionDeriveRequest, 58 | ) (*types.ConstructionDeriveResponse, *types.Error) { 59 | if terr := cmn.ValidateNetworkIdentifier(ctx, request.NetworkIdentifier); terr != nil { 60 | return nil, terr 61 | } 62 | 63 | if request.PublicKey == nil { 64 | terr := cmn.ErrInvalidInputParam 65 | terr.Message += "public key is not provided" 66 | return nil, terr 67 | } 68 | 69 | if len(request.PublicKey.Bytes) == 0 || request.PublicKey.CurveType != CurveType { 70 | terr := cmn.ErrInvalidInputParam 71 | terr.Message += "unsupported public key curve type" 72 | return nil, terr 73 | } 74 | 75 | pubkey, err := decompressPubkey(request.PublicKey.Bytes) 76 | if err != nil { 77 | terr := cmn.ErrInvalidInputParam 78 | terr.Message += "Unable to decompress public key: " + err.Error() 79 | return nil, terr 80 | } 81 | 82 | addr := pubkeyToAddress(*pubkey) 83 | return &types.ConstructionDeriveResponse{ 84 | AccountIdentifier: &types.AccountIdentifier{ 85 | Address: addr.Hex(), 86 | }, 87 | }, nil 88 | } 89 | 90 | // ConstructionPreprocess implements the /construction/preprocess endpoint. 91 | func (s *constructionAPIService) ConstructionPreprocess( 92 | ctx context.Context, 93 | request *types.ConstructionPreprocessRequest, 94 | ) (*types.ConstructionPreprocessResponse, *types.Error) { 95 | if err := cmn.ValidateNetworkIdentifier(ctx, request.NetworkIdentifier); err != nil { 96 | return nil, err 97 | } 98 | 99 | options := make(map[string]interface{}) 100 | 101 | matches, e := getOperationDescriptions(request.Operations) 102 | if e != nil { 103 | return nil, e 104 | } 105 | 106 | if len(matches) == 2 { 107 | options["type"] = cmn.SmartContractTx 108 | } else if len(matches) == 5 { // SendTx 109 | fromThetaOp, _ := matches[0].First() 110 | fromTFuelOp, _ := matches[1].First() 111 | toThetaOp, _ := matches[2].First() 112 | toTFuelOp, _ := matches[3].First() 113 | feeOp, feeWei := matches[4].First() 114 | 115 | if fromThetaOp.Account.Address != fromTFuelOp.Account.Address || fromTFuelOp.Account.Address != feeOp.Account.Address { 116 | terr := cmn.ErrServiceInternal 117 | terr.Message += "from address not matching" 118 | return nil, terr 119 | } 120 | 121 | if toThetaOp.Account.Address != toTFuelOp.Account.Address { 122 | terr := cmn.ErrServiceInternal 123 | terr.Message += "to address not matching" 124 | return nil, terr 125 | } 126 | 127 | if fromTFuelOp.Account.Address == toTFuelOp.Account.Address { 128 | terr := cmn.ErrServiceInternal 129 | terr.Message += "from and to accounts are the same" 130 | return nil, terr 131 | } 132 | 133 | options["type"] = cmn.SendTx 134 | options["fee"] = feeWei 135 | } else { 136 | err := cmn.ErrServiceInternal 137 | err.Message += "invalid number of operations" 138 | return nil, err 139 | } 140 | 141 | fromOp, _ := matches[0].First() 142 | options["signer"] = fromOp.Account.Address 143 | 144 | if gasLimit, ok := request.Metadata["gas_limit"]; ok { 145 | options["gas_limit"] = gasLimit 146 | } 147 | if gasPrice, ok := request.Metadata["gas_price"]; ok { 148 | options["gas_price"] = gasPrice 149 | } 150 | if data, ok := request.Metadata["data"]; ok { 151 | options["data"] = data 152 | } 153 | 154 | return &types.ConstructionPreprocessResponse{ 155 | Options: options, 156 | }, nil 157 | } 158 | 159 | // ConstructionMetadata implements the /construction/metadata endpoint. 160 | func (s *constructionAPIService) ConstructionMetadata( 161 | ctx context.Context, 162 | request *types.ConstructionMetadataRequest, 163 | ) (*types.ConstructionMetadataResponse, *types.Error) { 164 | if !strings.EqualFold(cmn.CfgRosettaModeOnline, viper.GetString(cmn.CfgRosettaMode)) { 165 | return nil, cmn.ErrUnavailableOffline 166 | } 167 | 168 | if err := cmn.ValidateNetworkIdentifier(ctx, request.NetworkIdentifier); err != nil { 169 | return nil, err 170 | } 171 | 172 | meta := make(map[string]interface{}) 173 | 174 | var ok bool 175 | var signer interface{} 176 | if signer, ok = request.Options["signer"]; !ok { 177 | terr := cmn.ErrInvalidInputParam 178 | terr.Message += "empty signer address" 179 | return nil, terr 180 | } 181 | 182 | rpcRes, rpcErr := s.client.Call("theta.GetAccount", GetAccountArgs{ 183 | Address: signer.(string), 184 | }) 185 | 186 | parse := func(jsonBytes []byte) (interface{}, error) { 187 | account := GetAccountResult{}.Account 188 | err := json.Unmarshal(jsonBytes, &account) 189 | if err != nil { 190 | return nil, err 191 | } 192 | return account.Sequence, nil 193 | } 194 | 195 | seq, err := cmn.HandleThetaRPCResponse(rpcRes, rpcErr, parse) 196 | if err != nil { 197 | return nil, cmn.ErrUnableToGetAccount 198 | } 199 | 200 | meta["sequence"] = seq.(uint64) + 1 201 | 202 | var txType interface{} 203 | 204 | if txType, ok = request.Options["type"]; !ok { 205 | terr := cmn.ErrInvalidInputParam 206 | terr.Message += "tx type missing in metadata" 207 | return nil, terr 208 | } 209 | 210 | // meta["type"] = txType 211 | 212 | var status *cmn.GetStatusResult 213 | suggestedFee := big.NewInt(0) 214 | 215 | if cmn.TxType(txType.(float64)) == cmn.SendTx { 216 | if fee, ok := request.Options["fee"]; ok { 217 | suggestedFee = new(big.Int).SetUint64(uint64(fee.(float64))) 218 | } 219 | if suggestedFee.Cmp(big.NewInt(0)) == 0 { 220 | status, err = cmn.GetStatus(s.client) 221 | if err != nil { 222 | terr := cmn.ErrInvalidInputParam 223 | terr.Message += "can't get blockchain status" 224 | return nil, terr 225 | } 226 | height := uint64(status.CurrentHeight) 227 | suggestedFee = ttypes.GetSendTxMinimumTransactionFeeTFuelWei(2, height) // only allow 1-to-1 transfer 228 | } 229 | meta["fee"] = suggestedFee 230 | } else if cmn.TxType(txType.(float64)) == cmn.SmartContractTx { 231 | if gasLimit, ok := request.Options["gas_limit"]; ok { 232 | meta["gas_limit"] = gasLimit 233 | } else { 234 | if status == nil { 235 | status, err = cmn.GetStatus(s.client) 236 | if err != nil { 237 | terr := cmn.ErrInvalidInputParam 238 | terr.Message += "can't get blockchain status" 239 | return nil, terr 240 | } 241 | } 242 | 243 | height := uint64(status.CurrentHeight) 244 | gasLimit = ttypes.GetMaxGasLimit(height).Uint64() 245 | meta["gas_limit"] = gasLimit 246 | } 247 | 248 | if gasPrice, ok := request.Options["gas_price"]; ok { 249 | meta["gas_price"] = gasPrice 250 | } else { 251 | if status == nil { 252 | status, err = cmn.GetStatus(s.client) 253 | if err != nil { 254 | terr := cmn.ErrInvalidInputParam 255 | terr.Message += "can't get blockchain status" 256 | return nil, terr 257 | } 258 | } 259 | 260 | height := uint64(status.CurrentHeight) 261 | gasPrice = ttypes.GetMinimumGasPrice(height).Uint64() 262 | meta["gas_price"] = gasPrice 263 | } 264 | 265 | if data, ok := request.Options["data"]; ok { 266 | meta["data"] = data 267 | } 268 | } 269 | 270 | return &types.ConstructionMetadataResponse{ 271 | Metadata: meta, 272 | SuggestedFee: []*types.Amount{ 273 | { 274 | Value: suggestedFee.String(), 275 | Currency: cmn.GetTFuelCurrency(), 276 | }, 277 | }, 278 | }, nil 279 | } 280 | 281 | // ConstructionPayloads implements the /construction/payloads endpoint. 282 | func (s *constructionAPIService) ConstructionPayloads( 283 | ctx context.Context, 284 | request *types.ConstructionPayloadsRequest, 285 | ) (*types.ConstructionPayloadsResponse, *types.Error) { 286 | if err := cmn.ValidateNetworkIdentifier(ctx, request.NetworkIdentifier); err != nil { 287 | return nil, err 288 | } 289 | 290 | var ok bool 291 | var err error 292 | var seq interface{} 293 | if seq, ok = request.Metadata["sequence"]; !ok { 294 | terr := cmn.ErrServiceInternal 295 | terr.Message += "missing tx sequence" 296 | return nil, terr 297 | } 298 | sequence := uint64(seq.(float64)) 299 | 300 | var tx ttypes.Tx 301 | 302 | matches, e := getOperationDescriptions(request.Operations) 303 | if e != nil { 304 | return nil, e 305 | } 306 | 307 | if len(request.Operations) == 2 { // SmartContractTx 308 | fromOp, fromTFuelWei := matches[0].First() 309 | fromTFuelWei = new(big.Int).Mul(fromTFuelWei, big.NewInt(-1)) 310 | from := ttypes.TxInput{ 311 | Address: common.HexToAddress(fromOp.Account.Address), 312 | Coins: ttypes.Coins{ 313 | ThetaWei: new(big.Int).SetUint64(0), 314 | TFuelWei: fromTFuelWei, 315 | }, 316 | Sequence: sequence, 317 | } 318 | 319 | toOp, toTFuelWei := matches[1].First() 320 | to := ttypes.TxOutput{ 321 | Address: common.HexToAddress(toOp.Account.Address), 322 | Coins: ttypes.Coins{ 323 | ThetaWei: new(big.Int).SetUint64(0), 324 | TFuelWei: toTFuelWei, 325 | }, 326 | } 327 | 328 | gasPriceStr := "0wei" 329 | if gasPrice, ok := request.Metadata["gas_price"]; ok { 330 | gasPriceStr = fmt.Sprintf("%f", gasPrice.(float64)) + "wei" 331 | } 332 | gasPrice, ok := ttypes.ParseCoinAmount(gasPriceStr) 333 | if !ok { 334 | terr := cmn.ErrServiceInternal 335 | terr.Message += "failed to parse gas price" 336 | return nil, terr 337 | } 338 | 339 | gasLimit := uint64(10000000) //TODO: 10000000 or 0? 340 | if gasLim, ok := request.Metadata["gas_limit"]; ok { 341 | gasLimit = uint64(gasLim.(float64)) 342 | if err != nil { 343 | terr := cmn.ErrServiceInternal 344 | terr.Message += "failed to parse gas limit" 345 | return nil, terr 346 | } 347 | } 348 | 349 | var dataStr string 350 | if datum, ok := request.Metadata["data"]; ok { 351 | dataStr = datum.(string) 352 | } 353 | data, err := hex.DecodeString(dataStr) 354 | if err != nil { 355 | terr := cmn.ErrServiceInternal 356 | terr.Message += "failed to parse data" 357 | return nil, terr 358 | } 359 | 360 | tx = &ttypes.SmartContractTx{ 361 | From: from, 362 | To: to, 363 | GasLimit: gasLimit, //uint64(meta["gas_limit"].(float64)), 364 | GasPrice: gasPrice, //big.NewInt(int64(meta["gas_price"].(float64))), 365 | Data: data, //request.Metadata["data"].([]byte), 366 | } 367 | 368 | } else if len(request.Operations) == 5 { // SendTx 369 | fromThetaOp, fromThetaWei := matches[0].First() 370 | _, fromTFuelWei := matches[1].First() 371 | toThetaOp, toThetaWei := matches[2].First() 372 | _, toTFuelWei := matches[3].First() 373 | _, feeWei := matches[4].First() // TODO: overwritten with request.Metadata["fee"] ? 374 | 375 | fromThetaWei = new(big.Int).Mul(fromThetaWei, big.NewInt(-1)) 376 | fromTFuelWei = new(big.Int).Mul(fromTFuelWei, big.NewInt(-1)) 377 | feeWei = new(big.Int).Mul(feeWei, big.NewInt(-1)) 378 | 379 | inputs := []ttypes.TxInput{ 380 | { 381 | Address: common.HexToAddress(fromThetaOp.Account.Address), 382 | Coins: ttypes.Coins{ 383 | ThetaWei: fromThetaWei, 384 | TFuelWei: new(big.Int).Add(fromTFuelWei, feeWei), 385 | }, 386 | Sequence: uint64(seq.(float64)), 387 | }, 388 | } 389 | 390 | outputs := []ttypes.TxOutput{ 391 | { 392 | Address: common.HexToAddress(toThetaOp.Account.Address), 393 | Coins: ttypes.Coins{ 394 | ThetaWei: toThetaWei, 395 | TFuelWei: toTFuelWei, 396 | }, 397 | }, 398 | } 399 | 400 | tx = &ttypes.SendTx{ 401 | Fee: ttypes.Coins{ 402 | ThetaWei: new(big.Int).SetUint64(0), 403 | TFuelWei: feeWei, 404 | }, 405 | Inputs: inputs, 406 | Outputs: outputs, 407 | } 408 | } else { 409 | err := cmn.ErrServiceInternal 410 | err.Message += "invalid number of operations" 411 | return nil, err 412 | } 413 | 414 | raw, err := ttypes.TxToBytes(tx) 415 | if err != nil { 416 | terr := cmn.ErrServiceInternal 417 | terr.Message += err.Error() 418 | return nil, terr 419 | } 420 | 421 | unsignedTx := hex.EncodeToString(raw) 422 | signBytes := tx.SignBytes(cmn.GetChainId()) 423 | 424 | return &types.ConstructionPayloadsResponse{ 425 | UnsignedTransaction: unsignedTx, 426 | Payloads: []*types.SigningPayload{ 427 | { 428 | AccountIdentifier: &types.AccountIdentifier{ 429 | Address: request.Operations[0].Account.Address, 430 | }, 431 | Bytes: crypto.Keccak256Hash(signBytes).Bytes(), 432 | SignatureType: SignatureType, 433 | }, 434 | }, 435 | }, nil 436 | } 437 | 438 | // ConstructionParse implements the /construction/parse endpoint. 439 | func (s *constructionAPIService) ConstructionParse( 440 | ctx context.Context, 441 | request *types.ConstructionParseRequest, 442 | ) (*types.ConstructionParseResponse, *types.Error) { 443 | if err := cmn.ValidateNetworkIdentifier(ctx, request.NetworkIdentifier); err != nil { 444 | return nil, err 445 | } 446 | 447 | rawTx, err := hex.DecodeString(request.Transaction) 448 | if err != nil { 449 | terr := cmn.ErrUnableToParseTx 450 | terr.Message += err.Error() 451 | return nil, terr 452 | } 453 | 454 | tx, err := ttypes.TxFromBytes(rawTx) 455 | if err != nil { 456 | terr := cmn.ErrUnableToParseTx 457 | terr.Message += err.Error() 458 | return nil, terr 459 | } 460 | 461 | var signer string 462 | var meta map[string]interface{} 463 | var ops []*types.Operation 464 | 465 | switch tx.(type) { 466 | case *ttypes.SendTx: 467 | tran := *tx.(*ttypes.SendTx) 468 | signer = tran.Inputs[0].Address.String() 469 | meta, ops = cmn.ParseSendTx(tran, nil, cmn.SendTx) 470 | case *ttypes.SmartContractTx: 471 | tran := *tx.(*ttypes.SmartContractTx) 472 | signer = tran.From.Address.String() 473 | meta, ops = cmn.ParseSmartContractTxForConstruction(tran, cmn.SmartContractTx) 474 | default: 475 | terr := cmn.ErrUnableToParseTx 476 | terr.Message += "unsupported tx type" 477 | return nil, terr 478 | } 479 | 480 | resp := &types.ConstructionParseResponse{ 481 | Operations: ops, 482 | Metadata: meta, 483 | } 484 | if request.Signed { 485 | resp.AccountIdentifierSigners = []*types.AccountIdentifier{ 486 | { 487 | Address: signer, 488 | }, 489 | } 490 | } 491 | 492 | return resp, nil 493 | } 494 | 495 | // ConstructionCombine implements the /construction/combine endpoint. 496 | func (s *constructionAPIService) ConstructionCombine( 497 | ctx context.Context, 498 | request *types.ConstructionCombineRequest, 499 | ) (*types.ConstructionCombineResponse, *types.Error) { 500 | if err := cmn.ValidateNetworkIdentifier(ctx, request.NetworkIdentifier); err != nil { 501 | return nil, err 502 | } 503 | 504 | rawTx, err := hex.DecodeString(request.UnsignedTransaction) 505 | if err != nil { 506 | terr := cmn.ErrUnableToParseTx 507 | terr.Message += err.Error() 508 | return nil, terr 509 | } 510 | 511 | tx, err := ttypes.TxFromBytes(rawTx) 512 | if err != nil { 513 | terr := cmn.ErrUnableToParseTx 514 | terr.Message += err.Error() 515 | return nil, terr 516 | } 517 | 518 | if len(request.Signatures) != 1 { 519 | terr := cmn.ErrInvalidInputParam 520 | terr.Message += "need exact 1 signature" 521 | return nil, terr 522 | } 523 | 524 | sig, err := crypto.SignatureFromBytes(request.Signatures[0].Bytes) 525 | if err != nil { 526 | terr := cmn.ErrInvalidInputParam 527 | terr.Message += fmt.Sprintf("Cannot convert signature from payload bytes") 528 | return nil, terr 529 | } 530 | 531 | signer := common.HexToAddress(request.Signatures[0].SigningPayload.AccountIdentifier.Address) 532 | 533 | // Check signatures 534 | signBytes := tx.SignBytes(cmn.GetChainId()) 535 | 536 | if !sig.Verify(signBytes, signer) { 537 | terr := cmn.ErrInvalidInputParam 538 | terr.Message += fmt.Sprintf("Signature verification failed, SignBytes: %v", hex.EncodeToString(signBytes)) 539 | return nil, terr 540 | } 541 | 542 | switch tx.(type) { 543 | case *ttypes.SendTx: 544 | tx.(*ttypes.SendTx).SetSignature(signer, sig) 545 | // tran := *tx.(*ttypes.SendTx) 546 | // tran.SetSignature(signer, sig) 547 | case *ttypes.SmartContractTx: 548 | tx.(*ttypes.SmartContractTx).SetSignature(signer, sig) 549 | default: 550 | terr := cmn.ErrUnableToParseTx 551 | terr.Message += "unsupported tx type" 552 | return nil, terr 553 | } 554 | 555 | raw, err := ttypes.TxToBytes(tx) 556 | if err != nil { 557 | terr := cmn.ErrInvalidInputParam 558 | terr.Message += "Failed to encode transaction" 559 | return nil, terr 560 | } 561 | 562 | return &types.ConstructionCombineResponse{ 563 | SignedTransaction: hex.EncodeToString(raw), 564 | }, nil 565 | } 566 | 567 | // ConstructionHash implements the /construction/hash endpoint. 568 | func (s *constructionAPIService) ConstructionHash( 569 | ctx context.Context, 570 | request *types.ConstructionHashRequest, 571 | ) (*types.TransactionIdentifierResponse, *types.Error) { 572 | if err := cmn.ValidateNetworkIdentifier(ctx, request.NetworkIdentifier); err != nil { 573 | return nil, err 574 | } 575 | 576 | rawTx, err := hex.DecodeString(request.SignedTransaction) 577 | if err != nil { 578 | terr := cmn.ErrInvalidInputParam 579 | terr.Message += "invalid signed transaction format: " + err.Error() 580 | return nil, terr 581 | } 582 | 583 | hash := crypto.Keccak256Hash(rawTx) 584 | 585 | return &types.TransactionIdentifierResponse{ 586 | TransactionIdentifier: &types.TransactionIdentifier{ 587 | Hash: hash.String(), 588 | }, 589 | }, nil 590 | } 591 | 592 | // ConstructionSubmit implements the /construction/submit endpoint. 593 | func (s *constructionAPIService) ConstructionSubmit( 594 | ctx context.Context, 595 | request *types.ConstructionSubmitRequest, 596 | ) (*types.TransactionIdentifierResponse, *types.Error) { 597 | if !strings.EqualFold(cmn.CfgRosettaModeOnline, viper.GetString(cmn.CfgRosettaMode)) { 598 | return nil, cmn.ErrUnavailableOffline 599 | } 600 | 601 | if err := cmn.ValidateNetworkIdentifier(ctx, request.NetworkIdentifier); err != nil { 602 | return nil, err 603 | } 604 | 605 | rpcRes, rpcErr := s.client.Call("theta.BroadcastRawTransactionAsync", BroadcastRawTransactionAsyncArgs{ 606 | TxBytes: request.SignedTransaction, 607 | }) 608 | 609 | parse := func(jsonBytes []byte) (interface{}, error) { 610 | broadcastResult := BroadcastRawTransactionAsyncResult{} 611 | err := json.Unmarshal(jsonBytes, &broadcastResult) 612 | if err != nil { 613 | return nil, err 614 | } 615 | 616 | resp := types.TransactionIdentifierResponse{} 617 | resp.TransactionIdentifier = &types.TransactionIdentifier{ 618 | Hash: broadcastResult.TxHash, 619 | } 620 | return resp, nil 621 | } 622 | 623 | res, err := cmn.HandleThetaRPCResponse(rpcRes, rpcErr, parse) 624 | if err != nil { 625 | terr := cmn.ErrUnableToSubmitTx 626 | terr.Message += err.Error() 627 | return nil, terr 628 | } 629 | 630 | ret, _ := res.(types.TransactionIdentifierResponse) 631 | return &ret, nil 632 | } 633 | 634 | // decompressPubkey parses a public key in the 33-byte compressed format. 635 | func decompressPubkey(pubkey []byte) (*ecdsa.PublicKey, error) { 636 | x, y := secp256k1.DecompressPubkey(pubkey) 637 | if x == nil { 638 | return nil, fmt.Errorf("invalid public key") 639 | } 640 | return &ecdsa.PublicKey{X: x, Y: y, Curve: s256()}, nil 641 | } 642 | 643 | // s256 returns an instance of the secp256k1 curve. 644 | func s256() elliptic.Curve { 645 | return secp256k1.S256() 646 | } 647 | 648 | func pubkeyToAddress(p ecdsa.PublicKey) common.Address { 649 | pubBytes := fromECDSAPub(&p) 650 | return common.BytesToAddress(keccak256(pubBytes[1:])[12:]) 651 | } 652 | 653 | func fromECDSAPub(pub *ecdsa.PublicKey) []byte { 654 | if pub == nil || pub.X == nil || pub.Y == nil { 655 | return nil 656 | } 657 | return elliptic.Marshal(s256(), pub.X, pub.Y) 658 | } 659 | 660 | // keccak256 calculates and returns the Keccak256 hash of the input data. 661 | func keccak256(data ...[]byte) []byte { 662 | d := sha3.NewKeccak256() 663 | for _, b := range data { 664 | d.Write(b) 665 | } 666 | return d.Sum(nil) 667 | } 668 | 669 | func getOperationDescriptions(operations []*types.Operation) (matches []*parser.Match, err *types.Error) { 670 | var e error 671 | if len(operations) == 2 { // SmartContractTx 672 | descriptions := &parser.Descriptions{ 673 | OperationDescriptions: []*parser.OperationDescription{ 674 | { 675 | Type: cmn.SmartContractTxFrom.String(), 676 | Account: &parser.AccountDescription{ 677 | Exists: true, 678 | }, 679 | Amount: &parser.AmountDescription{ 680 | Exists: true, 681 | Sign: parser.NegativeOrZeroAmountSign, 682 | Currency: cmn.GetTFuelCurrency(), 683 | }, 684 | }, 685 | { 686 | Type: cmn.SmartContractTxTo.String(), 687 | Account: &parser.AccountDescription{ 688 | Exists: true, 689 | }, 690 | Amount: &parser.AmountDescription{ 691 | Exists: true, 692 | Sign: parser.PositiveOrZeroAmountSign, 693 | Currency: cmn.GetTFuelCurrency(), 694 | }, 695 | }, 696 | }, 697 | ErrUnmatched: true, 698 | } 699 | 700 | matches, e = parser.MatchOperations(descriptions, operations) 701 | if e != nil { 702 | err = cmn.ErrServiceInternal 703 | err.Message += e.Error() 704 | } 705 | } else if len(operations) == 5 { // SendTx 706 | descriptions := &parser.Descriptions{ 707 | OperationDescriptions: []*parser.OperationDescription{ 708 | { 709 | Type: cmn.SendTxInput.String(), 710 | Account: &parser.AccountDescription{ 711 | Exists: true, 712 | }, 713 | Amount: &parser.AmountDescription{ 714 | Exists: true, 715 | Sign: parser.NegativeOrZeroAmountSign, 716 | Currency: cmn.GetThetaCurrency(), 717 | }, 718 | }, 719 | { 720 | Type: cmn.SendTxInput.String(), 721 | Account: &parser.AccountDescription{ 722 | Exists: true, 723 | }, 724 | Amount: &parser.AmountDescription{ 725 | Exists: true, 726 | Sign: parser.NegativeOrZeroAmountSign, 727 | Currency: cmn.GetTFuelCurrency(), 728 | }, 729 | }, 730 | { 731 | Type: cmn.SendTxOutput.String(), 732 | Account: &parser.AccountDescription{ 733 | Exists: true, 734 | }, 735 | Amount: &parser.AmountDescription{ 736 | Exists: true, 737 | Sign: parser.PositiveOrZeroAmountSign, 738 | Currency: cmn.GetThetaCurrency(), 739 | }, 740 | }, 741 | { 742 | Type: cmn.SendTxOutput.String(), 743 | Account: &parser.AccountDescription{ 744 | Exists: true, 745 | }, 746 | Amount: &parser.AmountDescription{ 747 | Exists: true, 748 | Sign: parser.PositiveOrZeroAmountSign, 749 | Currency: cmn.GetTFuelCurrency(), 750 | }, 751 | }, 752 | { 753 | Type: cmn.TxFee.String(), 754 | Account: &parser.AccountDescription{ 755 | Exists: true, 756 | }, 757 | Amount: &parser.AmountDescription{ 758 | Exists: true, 759 | Sign: parser.NegativeOrZeroAmountSign, 760 | Currency: cmn.GetTFuelCurrency(), 761 | }, 762 | }, 763 | }, 764 | ErrUnmatched: true, 765 | } 766 | 767 | matches, e = parser.MatchOperations(descriptions, operations) 768 | if e != nil { 769 | err = cmn.ErrServiceInternal 770 | err.Message += e.Error() 771 | } 772 | } else { 773 | err = cmn.ErrServiceInternal 774 | err.Message += "invalid number of operations" 775 | } 776 | 777 | return 778 | } 779 | -------------------------------------------------------------------------------- /common/utils.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "math/big" 8 | "strings" 9 | 10 | "github.com/spf13/viper" 11 | 12 | rpcc "github.com/ybbus/jsonrpc" 13 | 14 | "github.com/coinbase/rosetta-sdk-go/types" 15 | log "github.com/sirupsen/logrus" 16 | "github.com/thetatoken/theta/blockchain" 17 | cmn "github.com/thetatoken/theta/common" 18 | "github.com/thetatoken/theta/core" 19 | "github.com/thetatoken/theta/crypto" 20 | "github.com/thetatoken/theta/crypto/bls" 21 | ttypes "github.com/thetatoken/theta/ledger/types" 22 | jrpc "github.com/ybbus/jsonrpc" 23 | ) 24 | 25 | var logger *log.Entry = log.WithFields(log.Fields{"prefix": "utils"}) 26 | 27 | const StakeWithdrawPrefix = "stake_withdraw" 28 | 29 | // ------------------------------ Chain ID ----------------------------------- 30 | var chainId string 31 | 32 | func GetChainId() string { 33 | return chainId 34 | } 35 | 36 | func SetChainId(chid string) { 37 | chainId = chid 38 | } 39 | 40 | // ------------------------------ Theta RPC ----------------------------------- 41 | 42 | func GetThetaRPCEndpoint() string { 43 | thetaRPCEndpoint := viper.GetString(CfgThetaRPCEndpoint) 44 | return thetaRPCEndpoint 45 | } 46 | 47 | func HandleThetaRPCResponse(rpcRes *rpcc.RPCResponse, rpcErr error, parse func(jsonBytes []byte) (interface{}, error)) (result interface{}, err error) { 48 | if rpcErr != nil { 49 | return nil, fmt.Errorf("failed to get theta RPC response: %v", rpcErr) 50 | } 51 | if rpcRes.Error != nil { 52 | return nil, fmt.Errorf("theta RPC returns an error: %v", rpcRes.Error) 53 | } 54 | 55 | var jsonBytes []byte 56 | jsonBytes, err = json.MarshalIndent(rpcRes.Result, "", " ") 57 | if err != nil { 58 | return nil, fmt.Errorf("failed to parse theta RPC response: %v, %s", err, string(jsonBytes)) 59 | } 60 | 61 | result, err = parse(jsonBytes) 62 | return 63 | } 64 | 65 | // ------------------------------ Validate Network Identifier ----------------------------------- 66 | 67 | func ValidateNetworkIdentifier(ctx context.Context, ni *types.NetworkIdentifier) *types.Error { 68 | if ni != nil { 69 | if !strings.EqualFold(ni.Blockchain, ChainName) { 70 | return ErrInvalidBlockchain 71 | } 72 | if ni.SubNetworkIdentifier != nil { 73 | return ErrInvalidSubnetwork 74 | } 75 | if !strings.EqualFold(ni.Network, GetChainId()) { 76 | return ErrInvalidNetwork 77 | } 78 | } else { 79 | return ErrMissingNID 80 | } 81 | return nil 82 | } 83 | 84 | // ------------------------------ GetStatus ----------------------------------- 85 | 86 | type GetStatusArgs struct{} 87 | 88 | type GetStatusResult struct { 89 | Address string `json:"address"` 90 | ChainID string `json:"chain_id"` 91 | PeerID string `json:"peer_id"` 92 | LatestFinalizedBlockHash cmn.Hash `json:"latest_finalized_block_hash"` 93 | LatestFinalizedBlockHeight cmn.JSONUint64 `json:"latest_finalized_block_height"` 94 | LatestFinalizedBlockTime *cmn.JSONBig `json:"latest_finalized_block_time"` 95 | LatestFinalizedBlockEpoch cmn.JSONUint64 `json:"latest_finalized_block_epoch"` 96 | CurrentEpoch cmn.JSONUint64 `json:"current_epoch"` 97 | CurrentHeight cmn.JSONUint64 `json:"current_height"` 98 | CurrentTime *cmn.JSONBig `json:"current_time"` 99 | Syncing bool `json:"syncing"` 100 | GenesisBlockHash cmn.Hash `json:"genesis_block_hash"` 101 | SnapshotBlockHeight cmn.JSONUint64 `json:"snapshot_block_height"` 102 | SnapshotBlockHash cmn.Hash `json:"snapshot_block_hash"` 103 | } 104 | 105 | func GetStatus(client jrpc.RPCClient) (*GetStatusResult, error) { 106 | rpcRes, rpcErr := client.Call("theta.GetStatus", GetStatusArgs{}) 107 | if rpcErr != nil { 108 | return nil, rpcErr 109 | } 110 | jsonBytes, err := json.MarshalIndent(rpcRes.Result, "", " ") 111 | if err != nil { 112 | return nil, fmt.Errorf("failed to parse theta RPC response: %v, %s", err, string(jsonBytes)) 113 | } 114 | 115 | trpcResult := GetStatusResult{} 116 | json.Unmarshal(jsonBytes, &trpcResult) 117 | 118 | return &trpcResult, nil 119 | } 120 | 121 | // ------------------------------ BlockIdentifier ----------------------------------- 122 | 123 | type GetBlockIdentifierByHashArgs struct { 124 | Hash cmn.Hash `json:"hash"` 125 | } 126 | 127 | type GetBlockIdentifierByHeightArgs struct { 128 | Height cmn.JSONUint64 `json:"height"` 129 | } 130 | 131 | type GetBlockIdentifierResultInner struct { 132 | Height cmn.JSONUint64 `json:"height"` 133 | Hash cmn.Hash `json:"hash"` 134 | } 135 | 136 | type GetBlocIdentifierResult struct { 137 | *GetBlockIdentifierResultInner 138 | } 139 | 140 | func GetBlockIdentifierByHeight(client jrpc.RPCClient, height cmn.JSONUint64) (*GetBlocIdentifierResult, *types.Error) { 141 | rpcRes, rpcErr := client.Call("theta.GetBlockByHeight", GetBlockIdentifierByHeightArgs{ 142 | Height: height, 143 | }) 144 | if rpcErr != nil { 145 | return nil, &types.Error{Message: fmt.Sprintf("failed to get block by height: %s", rpcErr.Error())} 146 | } 147 | if rpcRes != nil && rpcRes.Error != nil { 148 | if rpcRes.Error.Code == -32000 { 149 | return nil, ErrUnableToGetBlk 150 | } else { 151 | return nil, &types.Error{Message: fmt.Sprintf("failed to get block by height: %s", rpcRes.Error)} 152 | } 153 | } 154 | return parseBlockIdentifierResult(rpcRes) 155 | } 156 | 157 | func GetBlockIdentifierByHash(client jrpc.RPCClient, hash string) (*GetBlocIdentifierResult, *types.Error) { 158 | rpcRes, rpcErr := client.Call("theta.GetBlock", GetBlockIdentifierByHashArgs{ 159 | Hash: cmn.HexToHash(hash), 160 | }) 161 | if rpcErr != nil { 162 | return nil, &types.Error{Message: fmt.Sprintf("failed to get block by hash: %s", rpcErr.Error())} 163 | } 164 | if rpcRes != nil && rpcRes.Error != nil { 165 | if rpcRes.Error.Code == -32000 { 166 | return nil, ErrUnableToGetBlk 167 | } else { 168 | return nil, &types.Error{Message: fmt.Sprintf("failed to get block by hash: %s", rpcRes.Error)} 169 | } 170 | } 171 | return parseBlockIdentifierResult(rpcRes) 172 | } 173 | 174 | func parseBlockIdentifierResult(rpcRes *jrpc.RPCResponse) (*GetBlocIdentifierResult, *types.Error) { 175 | jsonBytes, err := json.MarshalIndent(rpcRes.Result, "", " ") 176 | if err != nil { 177 | return nil, &types.Error{Message: fmt.Sprintf("failed to parse theta RPC response: %v, %s", err, string(jsonBytes))} 178 | } 179 | 180 | trpcResult := GetBlocIdentifierResult{} 181 | json.Unmarshal(jsonBytes, &trpcResult) 182 | 183 | if trpcResult.GetBlockIdentifierResultInner == nil { 184 | return nil, ErrUnableToGetBlk 185 | } 186 | return &trpcResult, nil 187 | } 188 | 189 | // ------------------------------ Tx ----------------------------------- 190 | 191 | func ParseCoinbaseTx(coinbaseTx ttypes.CoinbaseTx, status *string, txType TxType) (metadata map[string]interface{}, ops []*types.Operation) { 192 | metadata = map[string]interface{}{ 193 | "type": txType, 194 | "block_height": coinbaseTx.BlockHeight, 195 | } 196 | 197 | sigBytes, _ := coinbaseTx.Proposer.Signature.MarshalJSON() 198 | op := types.Operation{ 199 | OperationIdentifier: &types.OperationIdentifier{Index: 0}, 200 | Type: CoinbaseTxProposer.String(), 201 | Account: &types.AccountIdentifier{Address: coinbaseTx.Proposer.Address.String()}, 202 | Amount: &types.Amount{Value: "0", Currency: GetTFuelCurrency()}, 203 | Metadata: map[string]interface{}{"sequence": coinbaseTx.Proposer.Sequence, "signature": sigBytes}, 204 | } 205 | if status != nil { 206 | op.Status = status 207 | } 208 | 209 | ops = []*types.Operation{&op} 210 | 211 | for i, output := range coinbaseTx.Outputs { 212 | outputOp := &types.Operation{ 213 | OperationIdentifier: &types.OperationIdentifier{Index: int64(i) + 1}, 214 | RelatedOperations: []*types.OperationIdentifier{{Index: 0}}, 215 | Type: CoinbaseTxOutput.String(), 216 | Account: &types.AccountIdentifier{Address: output.Address.String()}, 217 | Amount: &types.Amount{Value: output.Coins.TFuelWei.String(), Currency: GetTFuelCurrency()}, 218 | } 219 | if status != nil { 220 | outputOp.Status = status 221 | } 222 | ops = append(ops, outputOp) 223 | } 224 | return 225 | } 226 | 227 | func ParseSlashTx(slashTx ttypes.SlashTx, status *string, txType TxType) (metadata map[string]interface{}, ops []*types.Operation) { 228 | metadata = map[string]interface{}{ 229 | "type": txType, 230 | "slashed_address": slashTx.SlashedAddress, 231 | "reserve_sequence": slashTx.ReserveSequence, 232 | "slash_proof": slashTx.SlashProof, 233 | } 234 | 235 | sigBytes, _ := slashTx.Proposer.Signature.MarshalJSON() 236 | op := types.Operation{ 237 | OperationIdentifier: &types.OperationIdentifier{Index: 0}, 238 | Type: SlashTxProposer.String(), 239 | Status: status, // same as block status 240 | Account: &types.AccountIdentifier{Address: slashTx.Proposer.Address.String()}, 241 | Amount: &types.Amount{Value: "0", Currency: GetThetaCurrency()}, 242 | Metadata: map[string]interface{}{"sequence": slashTx.Proposer.Sequence, "signature": sigBytes}, 243 | } 244 | if status != nil { 245 | op.Status = status 246 | } 247 | ops = []*types.Operation{&op} 248 | return 249 | } 250 | 251 | func ParseSendTx(sendTx ttypes.SendTx, status *string, txType TxType) (metadata map[string]interface{}, ops []*types.Operation) { 252 | metadata = map[string]interface{}{ 253 | "type": txType, 254 | "fee": sendTx.Fee, 255 | } 256 | 257 | var i int64 258 | var inputAddr string 259 | 260 | for _, input := range sendTx.Inputs { 261 | sigBytes, _ := input.Signature.MarshalJSON() 262 | inputAddr = input.Address.String() 263 | 264 | if input.Coins.ThetaWei == nil { 265 | input.Coins.ThetaWei = big.NewInt(0) 266 | } 267 | thetaWei := new(big.Int).Mul(input.Coins.ThetaWei, big.NewInt(-1)).String() 268 | thetaInputOp := &types.Operation{ 269 | OperationIdentifier: &types.OperationIdentifier{Index: i}, 270 | Type: SendTxInput.String(), 271 | Account: &types.AccountIdentifier{Address: input.Address.String()}, 272 | Amount: &types.Amount{Value: thetaWei, Currency: GetThetaCurrency()}, 273 | Metadata: map[string]interface{}{"sequence": input.Sequence, "signature": sigBytes}, 274 | } 275 | if status != nil { 276 | thetaInputOp.Status = status 277 | } 278 | if i > 0 { 279 | thetaInputOp.RelatedOperations = []*types.OperationIdentifier{{Index: i - 1}} 280 | } 281 | ops = append(ops, thetaInputOp) 282 | i++ 283 | 284 | if input.Coins.TFuelWei == nil { 285 | input.Coins.TFuelWei = big.NewInt(0) 286 | } 287 | tfuelInput := new(big.Int).Sub(input.Coins.TFuelWei, sendTx.Fee.TFuelWei) 288 | tfuelWei := new(big.Int).Mul(tfuelInput, big.NewInt(-1)).String() 289 | tfuelInputOp := &types.Operation{ 290 | OperationIdentifier: &types.OperationIdentifier{Index: i}, 291 | Type: SendTxInput.String(), 292 | Account: &types.AccountIdentifier{Address: input.Address.String()}, 293 | Amount: &types.Amount{Value: tfuelWei, Currency: GetTFuelCurrency()}, 294 | Metadata: map[string]interface{}{"sequence": input.Sequence, "signature": sigBytes}, 295 | } 296 | if status != nil { 297 | tfuelInputOp.Status = status 298 | } 299 | if i > 0 { 300 | tfuelInputOp.RelatedOperations = []*types.OperationIdentifier{{Index: i - 1}} 301 | } 302 | ops = append(ops, tfuelInputOp) 303 | i++ 304 | } 305 | 306 | for _, output := range sendTx.Outputs { 307 | if output.Coins.ThetaWei == nil { 308 | output.Coins.ThetaWei = big.NewInt(0) 309 | } 310 | thetaWei := output.Coins.ThetaWei.String() 311 | thetaOutputOp := &types.Operation{ 312 | OperationIdentifier: &types.OperationIdentifier{Index: i}, 313 | RelatedOperations: []*types.OperationIdentifier{{Index: i - 1}}, 314 | Type: SendTxOutput.String(), 315 | Account: &types.AccountIdentifier{Address: output.Address.String()}, 316 | Amount: &types.Amount{Value: thetaWei, Currency: GetThetaCurrency()}, 317 | } 318 | if status != nil { 319 | thetaOutputOp.Status = status 320 | } 321 | if i > 0 { 322 | thetaOutputOp.RelatedOperations = []*types.OperationIdentifier{{Index: i - 1}} 323 | } 324 | ops = append(ops, thetaOutputOp) 325 | i++ 326 | 327 | if output.Coins.TFuelWei == nil { 328 | output.Coins.TFuelWei = big.NewInt(0) 329 | } 330 | tfuelWei := output.Coins.TFuelWei.String() 331 | tfuelOutputOp := &types.Operation{ 332 | OperationIdentifier: &types.OperationIdentifier{Index: i}, 333 | RelatedOperations: []*types.OperationIdentifier{{Index: i - 1}}, 334 | Type: SendTxOutput.String(), 335 | Account: &types.AccountIdentifier{Address: output.Address.String()}, 336 | Amount: &types.Amount{Value: tfuelWei, Currency: GetTFuelCurrency()}, 337 | } 338 | if status != nil { 339 | tfuelOutputOp.Status = status 340 | } 341 | if i > 0 { 342 | tfuelOutputOp.RelatedOperations = []*types.OperationIdentifier{{Index: i - 1}} 343 | } 344 | ops = append(ops, tfuelOutputOp) 345 | i++ 346 | } 347 | 348 | fee := new(big.Int).Mul(sendTx.Fee.TFuelWei, big.NewInt(-1)).String() 349 | feeOp := &types.Operation{ 350 | OperationIdentifier: &types.OperationIdentifier{Index: i}, 351 | Type: TxFee.String(), 352 | Account: &types.AccountIdentifier{Address: inputAddr}, 353 | Amount: &types.Amount{Value: fee, Currency: GetTFuelCurrency()}, 354 | } 355 | if status != nil { 356 | feeOp.Status = status 357 | } 358 | if i > 0 { 359 | feeOp.RelatedOperations = []*types.OperationIdentifier{{Index: i - 1}} 360 | } 361 | ops = append(ops, feeOp) 362 | 363 | return 364 | } 365 | 366 | func ParseReserveFundTx(reserveFundTx ttypes.ReserveFundTx, status *string, txType TxType) (metadata map[string]interface{}, ops []*types.Operation) { 367 | metadata = map[string]interface{}{ 368 | "type": txType, 369 | "collateral": reserveFundTx.Collateral, 370 | "resource_ids": reserveFundTx.ResourceIDs, 371 | "duration": reserveFundTx.Duration, 372 | } 373 | 374 | sigBytes, _ := reserveFundTx.Source.Signature.MarshalJSON() 375 | var i int64 376 | 377 | if reserveFundTx.Source.Coins.ThetaWei != nil && reserveFundTx.Source.Coins.ThetaWei != big.NewInt(0) { 378 | op := &types.Operation{ 379 | OperationIdentifier: &types.OperationIdentifier{Index: i}, 380 | Type: ReserveFundTxSource.String(), 381 | Account: &types.AccountIdentifier{Address: reserveFundTx.Source.Address.String()}, 382 | Amount: &types.Amount{Value: new(big.Int).Mul(reserveFundTx.Source.Coins.ThetaWei, big.NewInt(-1)).String(), Currency: GetThetaCurrency()}, 383 | Metadata: map[string]interface{}{"sequence": reserveFundTx.Source.Sequence, "signature": sigBytes}, 384 | } 385 | if status != nil { 386 | op.Status = status 387 | } 388 | ops = append(ops, op) 389 | i++ 390 | } 391 | 392 | if reserveFundTx.Source.Coins.TFuelWei != nil && reserveFundTx.Source.Coins.TFuelWei != big.NewInt(0) { 393 | op := &types.Operation{ 394 | OperationIdentifier: &types.OperationIdentifier{Index: i}, 395 | Type: ReserveFundTxSource.String(), 396 | Account: &types.AccountIdentifier{Address: reserveFundTx.Source.Address.String()}, 397 | Amount: &types.Amount{Value: new(big.Int).Mul(reserveFundTx.Source.Coins.TFuelWei, big.NewInt(-1)).String(), Currency: GetTFuelCurrency()}, 398 | Metadata: map[string]interface{}{"sequence": reserveFundTx.Source.Sequence, "signature": sigBytes}, 399 | } 400 | if status != nil { 401 | op.Status = status 402 | } 403 | if i > 0 { 404 | op.RelatedOperations = []*types.OperationIdentifier{{Index: i - 1}} 405 | } 406 | ops = append(ops, op) 407 | i++ 408 | } 409 | 410 | fee := &types.Operation{ 411 | OperationIdentifier: &types.OperationIdentifier{Index: i}, 412 | Type: TxFee.String(), 413 | Account: &types.AccountIdentifier{Address: reserveFundTx.Source.Address.String()}, 414 | Amount: &types.Amount{Value: new(big.Int).Mul(reserveFundTx.Fee.TFuelWei, big.NewInt(-1)).String(), Currency: GetTFuelCurrency()}, 415 | RelatedOperations: []*types.OperationIdentifier{{Index: 1}}, 416 | } 417 | if status != nil { 418 | fee.Status = status 419 | } 420 | if i > 0 { 421 | fee.RelatedOperations = []*types.OperationIdentifier{{Index: i - 1}} 422 | } 423 | ops = append(ops, fee) 424 | 425 | return 426 | } 427 | 428 | func ParseReleaseFundTx(releaseFundTx ttypes.ReleaseFundTx, status *string, txType TxType) (metadata map[string]interface{}, ops []*types.Operation) { 429 | metadata = map[string]interface{}{ 430 | "type": txType, 431 | "reserve_sequence": releaseFundTx.ReserveSequence, 432 | } 433 | 434 | sigBytes, _ := releaseFundTx.Source.Signature.MarshalJSON() 435 | var i int64 436 | 437 | if releaseFundTx.Source.Coins.ThetaWei != nil && releaseFundTx.Source.Coins.ThetaWei != big.NewInt(0) { 438 | op := &types.Operation{ 439 | OperationIdentifier: &types.OperationIdentifier{Index: i}, 440 | Type: ReleaseFundTxSource.String(), 441 | Account: &types.AccountIdentifier{Address: releaseFundTx.Source.Address.String()}, 442 | Amount: &types.Amount{Value: releaseFundTx.Source.Coins.ThetaWei.String(), Currency: GetThetaCurrency()}, 443 | Metadata: map[string]interface{}{"sequence": releaseFundTx.Source.Sequence, "signature": sigBytes}, 444 | } 445 | if status != nil { 446 | op.Status = status 447 | } 448 | ops = append(ops, op) 449 | i++ 450 | } 451 | 452 | if releaseFundTx.Source.Coins.TFuelWei != nil && releaseFundTx.Source.Coins.TFuelWei != big.NewInt(0) { 453 | op := &types.Operation{ 454 | OperationIdentifier: &types.OperationIdentifier{Index: i}, 455 | Type: ReleaseFundTxSource.String(), 456 | Account: &types.AccountIdentifier{Address: releaseFundTx.Source.Address.String()}, 457 | Amount: &types.Amount{Value: new(big.Int).Mul(releaseFundTx.Source.Coins.TFuelWei, big.NewInt(-1)).String(), Currency: GetTFuelCurrency()}, 458 | Metadata: map[string]interface{}{"sequence": releaseFundTx.Source.Sequence, "signature": sigBytes}, 459 | } 460 | if status != nil { 461 | op.Status = status 462 | } 463 | if i > 0 { 464 | op.RelatedOperations = []*types.OperationIdentifier{{Index: i - 1}} 465 | } 466 | ops = append(ops, op) 467 | i++ 468 | } 469 | 470 | fee := &types.Operation{ 471 | OperationIdentifier: &types.OperationIdentifier{Index: i}, 472 | Type: TxFee.String(), 473 | Account: &types.AccountIdentifier{Address: releaseFundTx.Source.Address.String()}, 474 | Amount: &types.Amount{Value: new(big.Int).Mul(releaseFundTx.Fee.TFuelWei, big.NewInt(-1)).String(), Currency: GetTFuelCurrency()}, 475 | RelatedOperations: []*types.OperationIdentifier{{Index: 1}}, 476 | } 477 | if status != nil { 478 | fee.Status = status 479 | } 480 | if i > 0 { 481 | fee.RelatedOperations = []*types.OperationIdentifier{{Index: i - 1}} 482 | } 483 | ops = append(ops, fee) 484 | 485 | return 486 | } 487 | 488 | func ParseServicePaymentTx(servicePaymentTx ttypes.ServicePaymentTx, status *string, txType TxType) (metadata map[string]interface{}, ops []*types.Operation) { 489 | metadata = map[string]interface{}{ 490 | "type": txType, 491 | "payment_sequence": servicePaymentTx.PaymentSequence, 492 | "reserve_sequence": servicePaymentTx.ReserveSequence, 493 | "resource_id": servicePaymentTx.ResourceID, 494 | } 495 | 496 | sigBytes, _ := servicePaymentTx.Source.Signature.MarshalJSON() 497 | 498 | sourceOp := &types.Operation{ 499 | OperationIdentifier: &types.OperationIdentifier{Index: 0}, 500 | Type: ServicePaymentTxSource.String(), 501 | Account: &types.AccountIdentifier{Address: servicePaymentTx.Source.Address.String()}, 502 | Amount: &types.Amount{Value: new(big.Int).Mul(servicePaymentTx.Source.Coins.TFuelWei, big.NewInt(-1)).String(), Currency: GetTFuelCurrency()}, 503 | Metadata: map[string]interface{}{"sequence": servicePaymentTx.Source.Sequence, "signature": sigBytes}, 504 | } 505 | 506 | targetOp := &types.Operation{ 507 | OperationIdentifier: &types.OperationIdentifier{Index: 1}, 508 | Type: ServicePaymentTxTarget.String(), 509 | Account: &types.AccountIdentifier{Address: servicePaymentTx.Target.Address.String()}, 510 | Amount: &types.Amount{Value: servicePaymentTx.Target.Coins.TFuelWei.String(), Currency: GetTFuelCurrency()}, 511 | RelatedOperations: []*types.OperationIdentifier{{Index: 0}}, 512 | } 513 | 514 | fee := &types.Operation{ 515 | OperationIdentifier: &types.OperationIdentifier{Index: 2}, 516 | Type: TxFee.String(), 517 | Account: &types.AccountIdentifier{Address: servicePaymentTx.Target.Address.String()}, 518 | Amount: &types.Amount{Value: new(big.Int).Mul(servicePaymentTx.Fee.TFuelWei, big.NewInt(-1)).String(), Currency: GetTFuelCurrency()}, 519 | RelatedOperations: []*types.OperationIdentifier{{Index: 1}}, 520 | } 521 | 522 | if status != nil { 523 | sourceOp.Status = status 524 | targetOp.Status = status 525 | fee.Status = status 526 | } 527 | ops = []*types.Operation{sourceOp, targetOp, fee} 528 | return 529 | } 530 | 531 | func ParseSplitRuleTx(splitRuleTx ttypes.SplitRuleTx, status *string, txType TxType) (metadata map[string]interface{}, ops []*types.Operation) { 532 | metadata = map[string]interface{}{ 533 | "type": txType, 534 | "resource_id": splitRuleTx.ResourceID, 535 | "splits": splitRuleTx.Splits, 536 | "duration": splitRuleTx.Duration, 537 | } 538 | 539 | sigBytes, _ := splitRuleTx.Initiator.Signature.MarshalJSON() 540 | op := &types.Operation{ 541 | OperationIdentifier: &types.OperationIdentifier{Index: 0}, 542 | Type: SplitRuleTxInitiator.String(), 543 | Account: &types.AccountIdentifier{Address: splitRuleTx.Initiator.Address.String()}, 544 | Amount: &types.Amount{Value: splitRuleTx.Initiator.Coins.TFuelWei.String(), Currency: GetTFuelCurrency()}, 545 | Metadata: map[string]interface{}{"sequence": splitRuleTx.Initiator.Sequence, "signature": sigBytes}, 546 | } 547 | 548 | fee := &types.Operation{ 549 | OperationIdentifier: &types.OperationIdentifier{Index: 1}, 550 | Type: TxFee.String(), 551 | Account: &types.AccountIdentifier{Address: splitRuleTx.Initiator.Address.String()}, 552 | Amount: &types.Amount{Value: new(big.Int).Mul(splitRuleTx.Fee.TFuelWei, big.NewInt(-1)).String(), Currency: GetTFuelCurrency()}, 553 | RelatedOperations: []*types.OperationIdentifier{{Index: 0}}, 554 | } 555 | 556 | if status != nil { 557 | op.Status = status 558 | fee.Status = status 559 | } 560 | ops = []*types.Operation{op, fee} 561 | return 562 | } 563 | 564 | func ParseSmartContractTxForConstruction(smartContractTx ttypes.SmartContractTx, txType TxType) (metadata map[string]interface{}, ops []*types.Operation) { 565 | metadata = map[string]interface{}{ 566 | "type": txType, 567 | "gas_limit": smartContractTx.GasLimit, 568 | "gas_price": smartContractTx.GasPrice, 569 | "data": smartContractTx.Data, 570 | } 571 | 572 | sigBytes, _ := smartContractTx.From.Signature.MarshalJSON() 573 | var i int64 574 | 575 | if smartContractTx.From.Coins.ThetaWei != nil && len(smartContractTx.From.Coins.ThetaWei.Bits()) != 0 { 576 | thetaFrom := types.Operation{ 577 | OperationIdentifier: &types.OperationIdentifier{Index: i}, 578 | Type: SmartContractTxFrom.String(), 579 | Account: &types.AccountIdentifier{Address: smartContractTx.From.Address.String()}, 580 | Amount: &types.Amount{Value: new(big.Int).Mul(smartContractTx.From.Coins.ThetaWei, big.NewInt(-1)).String(), Currency: GetThetaCurrency()}, 581 | Metadata: map[string]interface{}{"sequence": smartContractTx.From.Sequence, "signature": sigBytes}, 582 | } 583 | if i > 0 { 584 | thetaFrom.RelatedOperations = []*types.OperationIdentifier{{Index: i - 1}} 585 | } 586 | ops = append(ops, &thetaFrom) 587 | i++ 588 | } 589 | if smartContractTx.From.Coins.TFuelWei != nil && len(smartContractTx.From.Coins.TFuelWei.Bits()) != 0 { 590 | tfuelFrom := types.Operation{ 591 | OperationIdentifier: &types.OperationIdentifier{Index: i}, 592 | Type: SmartContractTxFrom.String(), 593 | Account: &types.AccountIdentifier{Address: smartContractTx.From.Address.String()}, 594 | Amount: &types.Amount{Value: new(big.Int).Mul(smartContractTx.From.Coins.TFuelWei, big.NewInt(-1)).String(), Currency: GetTFuelCurrency()}, 595 | Metadata: map[string]interface{}{"sequence": smartContractTx.From.Sequence, "signature": sigBytes}, 596 | } 597 | if i > 0 { 598 | tfuelFrom.RelatedOperations = []*types.OperationIdentifier{{Index: i - 1}} 599 | } 600 | ops = append(ops, &tfuelFrom) 601 | i++ 602 | } 603 | 604 | if smartContractTx.To.Coins.ThetaWei != nil && len(smartContractTx.To.Coins.ThetaWei.Bits()) != 0 { 605 | thetaTo := types.Operation{ 606 | OperationIdentifier: &types.OperationIdentifier{Index: i}, 607 | Type: SmartContractTxTo.String(), 608 | Account: &types.AccountIdentifier{Address: smartContractTx.To.Address.String()}, 609 | Amount: &types.Amount{Value: smartContractTx.To.Coins.ThetaWei.String(), Currency: GetThetaCurrency()}, 610 | } 611 | if i > 0 { 612 | thetaTo.RelatedOperations = []*types.OperationIdentifier{{Index: i - 1}} 613 | } 614 | ops = append(ops, &thetaTo) 615 | i++ 616 | } 617 | if smartContractTx.To.Coins.TFuelWei != nil && len(smartContractTx.To.Coins.TFuelWei.Bits()) != 0 { 618 | tfuelTo := types.Operation{ 619 | OperationIdentifier: &types.OperationIdentifier{Index: i}, 620 | Type: SmartContractTxTo.String(), 621 | Account: &types.AccountIdentifier{Address: smartContractTx.To.Address.String()}, 622 | Amount: &types.Amount{Value: smartContractTx.To.Coins.TFuelWei.String(), Currency: GetTFuelCurrency()}, 623 | } 624 | if i > 0 { 625 | tfuelTo.RelatedOperations = []*types.OperationIdentifier{{Index: i - 1}} 626 | } 627 | ops = append(ops, &tfuelTo) 628 | i++ 629 | } 630 | return 631 | } 632 | 633 | func ParseSmartContractTx(smartContractTx ttypes.SmartContractTx, status *string, txType TxType, gasUsed uint64, balanceChanges *blockchain.TxBalanceChangesEntry) (metadata map[string]interface{}, ops []*types.Operation) { 634 | metadata = map[string]interface{}{ 635 | "type": txType, 636 | "gas_limit": smartContractTx.GasLimit, 637 | "gas_price": smartContractTx.GasPrice, 638 | "data": smartContractTx.Data, 639 | } 640 | 641 | var i int64 642 | 643 | for _, balanceChange := range balanceChanges.BalanceChanges { 644 | if balanceChange.TokenType > 1 { 645 | continue 646 | } 647 | 648 | var currency *types.Currency 649 | if balanceChange.TokenType == 0 { 650 | currency = GetThetaCurrency() 651 | } else { 652 | currency = GetTFuelCurrency() 653 | } 654 | 655 | amount := "0" 656 | var opType string 657 | if balanceChange.IsNegative { 658 | amount = new(big.Int).Mul(balanceChange.Delta, big.NewInt(-1)).String() 659 | opType = SmartContractTxFrom.String() 660 | } else { 661 | amount = balanceChange.Delta.String() 662 | opType = SmartContractTxTo.String() 663 | } 664 | 665 | op := &types.Operation{ 666 | OperationIdentifier: &types.OperationIdentifier{Index: i}, 667 | Type: opType, 668 | Account: &types.AccountIdentifier{Address: balanceChange.Address.String()}, 669 | Amount: &types.Amount{Value: amount, Currency: currency}, 670 | } 671 | 672 | if status != nil { 673 | op.Status = status 674 | } 675 | if i > 0 { 676 | op.RelatedOperations = []*types.OperationIdentifier{{Index: i - 1}} 677 | } 678 | ops = append(ops, op) 679 | i++ 680 | } 681 | 682 | if gasUsed != 0 { 683 | txFee := new(big.Int).Mul(new(big.Int).Mul(smartContractTx.GasPrice, new(big.Int).SetUint64(gasUsed)), big.NewInt(-1)).String() 684 | fee := &types.Operation{ 685 | OperationIdentifier: &types.OperationIdentifier{Index: i}, 686 | Type: TxFee.String(), 687 | Account: &types.AccountIdentifier{Address: smartContractTx.From.Address.String()}, 688 | Amount: &types.Amount{Value: txFee, Currency: GetTFuelCurrency()}, 689 | } 690 | if status != nil { 691 | fee.Status = status 692 | } 693 | if i > 0 { 694 | fee.RelatedOperations = []*types.OperationIdentifier{{Index: i - 1}} 695 | } 696 | ops = append(ops, fee) 697 | } 698 | 699 | return 700 | } 701 | 702 | func ParseDepositStakeTx(depositStakeTx ttypes.DepositStakeTxV2, status *string, txType TxType) (metadata map[string]interface{}, ops []*types.Operation) { 703 | metadata = map[string]interface{}{ 704 | "type": txType, 705 | "purpose": depositStakeTx.Purpose, 706 | } 707 | if depositStakeTx.BlsPubkey != nil { 708 | metadata["bls_pub_key"] = depositStakeTx.BlsPubkey 709 | } 710 | if depositStakeTx.BlsPop != nil { 711 | metadata["bls_pop"] = depositStakeTx.BlsPop 712 | } 713 | if depositStakeTx.HolderSig != nil { 714 | metadata["holder_sig"] = depositStakeTx.HolderSig 715 | } 716 | 717 | sigBytes, _ := depositStakeTx.Source.Signature.MarshalJSON() 718 | var i int64 719 | 720 | if depositStakeTx.Source.Coins.ThetaWei != nil && depositStakeTx.Source.Coins.ThetaWei != big.NewInt(0) { 721 | thetaSource := &types.Operation{ 722 | OperationIdentifier: &types.OperationIdentifier{Index: i}, 723 | Type: DepositStakeTxSource.String(), 724 | Account: &types.AccountIdentifier{Address: depositStakeTx.Source.Address.String()}, 725 | Amount: &types.Amount{Value: new(big.Int).Mul(depositStakeTx.Source.Coins.ThetaWei, big.NewInt(-1)).String(), Currency: GetThetaCurrency()}, 726 | Metadata: map[string]interface{}{"sequence": depositStakeTx.Source.Sequence, "signature": sigBytes}, 727 | } 728 | if status != nil { 729 | thetaSource.Status = status 730 | } 731 | if i > 0 { 732 | thetaSource.RelatedOperations = []*types.OperationIdentifier{{Index: i - 1}} 733 | } 734 | ops = append(ops, thetaSource) 735 | i++ 736 | } 737 | 738 | if depositStakeTx.Source.Coins.TFuelWei != nil && depositStakeTx.Source.Coins.TFuelWei != big.NewInt(0) { 739 | tfuelSource := &types.Operation{ 740 | OperationIdentifier: &types.OperationIdentifier{Index: i}, 741 | Type: DepositStakeTxSource.String(), 742 | Account: &types.AccountIdentifier{Address: depositStakeTx.Source.Address.String()}, 743 | Amount: &types.Amount{Value: new(big.Int).Mul(depositStakeTx.Source.Coins.TFuelWei, big.NewInt(-1)).String(), Currency: GetTFuelCurrency()}, 744 | Metadata: map[string]interface{}{"sequence": depositStakeTx.Source.Sequence, "signature": sigBytes}, 745 | } 746 | if status != nil { 747 | tfuelSource.Status = status 748 | } 749 | if i > 0 { 750 | tfuelSource.RelatedOperations = []*types.OperationIdentifier{{Index: i - 1}} 751 | } 752 | ops = append(ops, tfuelSource) 753 | i++ 754 | } 755 | 756 | fee := &types.Operation{ 757 | OperationIdentifier: &types.OperationIdentifier{Index: i}, 758 | Type: TxFee.String(), 759 | Account: &types.AccountIdentifier{Address: depositStakeTx.Source.Address.String()}, 760 | Amount: &types.Amount{Value: new(big.Int).Mul(depositStakeTx.Fee.TFuelWei, big.NewInt(-1)).String(), Currency: GetTFuelCurrency()}, 761 | } 762 | if status != nil { 763 | fee.Status = status 764 | } 765 | if i > 0 { 766 | fee.RelatedOperations = []*types.OperationIdentifier{{Index: i - 1}} 767 | } 768 | ops = append(ops, fee) 769 | 770 | return 771 | } 772 | 773 | func ParseWithdrawStakeTx(withdrawStakeTx ttypes.WithdrawStakeTx, status *string, txType TxType) (metadata map[string]interface{}, ops []*types.Operation) { 774 | metadata = map[string]interface{}{ 775 | "type": txType, 776 | "purpose": withdrawStakeTx.Purpose, 777 | } 778 | 779 | fee := &types.Operation{ 780 | OperationIdentifier: &types.OperationIdentifier{Index: 0}, 781 | Type: TxFee.String(), 782 | Account: &types.AccountIdentifier{Address: withdrawStakeTx.Source.Address.String()}, 783 | Amount: &types.Amount{Value: new(big.Int).Mul(withdrawStakeTx.Fee.TFuelWei, big.NewInt(-1)).String(), Currency: GetTFuelCurrency()}, 784 | } 785 | if status != nil { 786 | fee.Status = status 787 | } 788 | ops = append(ops, fee) 789 | return 790 | } 791 | 792 | func ParseReturnStakeTx(withdrawStakeTx ttypes.WithdrawStakeTx, status *string, txType TxType) (metadata map[string]interface{}, ops []*types.Operation) { 793 | metadata = map[string]interface{}{ 794 | "type": txType, 795 | "purpose": withdrawStakeTx.Purpose, 796 | } 797 | 798 | sigBytes, _ := withdrawStakeTx.Source.Signature.MarshalJSON() 799 | var i int64 800 | 801 | if withdrawStakeTx.Source.Coins.ThetaWei != nil && withdrawStakeTx.Source.Coins.ThetaWei != big.NewInt(0) { 802 | thetaSource := &types.Operation{ 803 | OperationIdentifier: &types.OperationIdentifier{Index: i}, 804 | Type: WithdrawStakeTxSource.String(), 805 | Account: &types.AccountIdentifier{Address: withdrawStakeTx.Source.Address.String()}, 806 | Amount: &types.Amount{Value: withdrawStakeTx.Source.Coins.ThetaWei.String(), Currency: GetThetaCurrency()}, 807 | Metadata: map[string]interface{}{"sequence": withdrawStakeTx.Source.Sequence, "signature": sigBytes}, 808 | } 809 | if status != nil { 810 | thetaSource.Status = status 811 | } 812 | if i > 0 { 813 | thetaSource.RelatedOperations = []*types.OperationIdentifier{{Index: i - 1}} 814 | } 815 | ops = append(ops, thetaSource) 816 | i++ 817 | } 818 | 819 | if withdrawStakeTx.Source.Coins.TFuelWei != nil && withdrawStakeTx.Source.Coins.TFuelWei != big.NewInt(0) { 820 | tfuelSource := &types.Operation{ 821 | OperationIdentifier: &types.OperationIdentifier{Index: i}, 822 | Type: WithdrawStakeTxSource.String(), 823 | Account: &types.AccountIdentifier{Address: withdrawStakeTx.Source.Address.String()}, 824 | Amount: &types.Amount{Value: withdrawStakeTx.Source.Coins.TFuelWei.String(), Currency: GetTFuelCurrency()}, 825 | Metadata: map[string]interface{}{"sequence": withdrawStakeTx.Source.Sequence, "signature": sigBytes}, 826 | } 827 | if status != nil { 828 | tfuelSource.Status = status 829 | } 830 | if i > 0 { 831 | tfuelSource.RelatedOperations = []*types.OperationIdentifier{{Index: i - 1}} 832 | } 833 | ops = append(ops, tfuelSource) 834 | } 835 | 836 | return 837 | } 838 | 839 | func ParseStakeRewardDistributionTx(stakeRewardDistributionTx ttypes.StakeRewardDistributionTx, status *string, txType TxType) (metadata map[string]interface{}, ops []*types.Operation) { 840 | metadata = map[string]interface{}{ 841 | "type": txType, 842 | "split_basis_point": stakeRewardDistributionTx.SplitBasisPoint, 843 | } 844 | 845 | var i int64 846 | 847 | if stakeRewardDistributionTx.Beneficiary.Coins.ThetaWei != nil && stakeRewardDistributionTx.Beneficiary.Coins.ThetaWei != big.NewInt(0) { 848 | thetaOutput := &types.Operation{ 849 | OperationIdentifier: &types.OperationIdentifier{Index: i}, 850 | RelatedOperations: []*types.OperationIdentifier{{Index: i - 1}}, 851 | Type: StakeRewardDistributionTxBeneficiary.String(), 852 | Account: &types.AccountIdentifier{Address: stakeRewardDistributionTx.Beneficiary.Address.String()}, 853 | Amount: &types.Amount{Value: stakeRewardDistributionTx.Beneficiary.Coins.ThetaWei.String(), Currency: GetThetaCurrency()}, 854 | } 855 | if status != nil { 856 | thetaOutput.Status = status 857 | } 858 | if i > 0 { 859 | thetaOutput.RelatedOperations = []*types.OperationIdentifier{{Index: i - 1}} 860 | } 861 | ops = append(ops, thetaOutput) 862 | i++ 863 | } 864 | 865 | if stakeRewardDistributionTx.Beneficiary.Coins.TFuelWei != nil && stakeRewardDistributionTx.Beneficiary.Coins.TFuelWei != big.NewInt(0) { 866 | tfuelOutput := &types.Operation{ 867 | OperationIdentifier: &types.OperationIdentifier{Index: i}, 868 | RelatedOperations: []*types.OperationIdentifier{{Index: i - 1}}, 869 | Type: StakeRewardDistributionTxBeneficiary.String(), 870 | Account: &types.AccountIdentifier{Address: stakeRewardDistributionTx.Beneficiary.Address.String()}, 871 | Amount: &types.Amount{Value: stakeRewardDistributionTx.Beneficiary.Coins.TFuelWei.String(), Currency: GetTFuelCurrency()}, 872 | } 873 | if status != nil { 874 | tfuelOutput.Status = status 875 | } 876 | if i > 0 { 877 | tfuelOutput.RelatedOperations = []*types.OperationIdentifier{{Index: i - 1}} 878 | } 879 | ops = append(ops, tfuelOutput) 880 | i++ 881 | } 882 | 883 | fee := &types.Operation{ 884 | OperationIdentifier: &types.OperationIdentifier{Index: i}, 885 | Type: TxFee.String(), 886 | Account: &types.AccountIdentifier{Address: stakeRewardDistributionTx.Holder.Address.String()}, 887 | Amount: &types.Amount{Value: new(big.Int).Mul(stakeRewardDistributionTx.Fee.TFuelWei, big.NewInt(-1)).String(), Currency: GetTFuelCurrency()}, 888 | } 889 | if status != nil { 890 | fee.Status = status 891 | } 892 | if i > 0 { 893 | fee.RelatedOperations = []*types.OperationIdentifier{{Index: i - 1}} 894 | } 895 | ops = append(ops, fee) 896 | 897 | return 898 | } 899 | 900 | func ParseTx(txType TxType, rawTx json.RawMessage, txHash cmn.Hash, status *string, gasUsed uint64, balanceChanges *blockchain.TxBalanceChangesEntry, db *LDBDatabase, stakeService *StakeService, blockHeight cmn.JSONUint64) types.Transaction { 901 | transaction := types.Transaction{ 902 | TransactionIdentifier: &types.TransactionIdentifier{Hash: txHash.String()}, 903 | } 904 | 905 | switch txType { 906 | case CoinbaseTx: 907 | coinbaseTx := ttypes.CoinbaseTx{} 908 | json.Unmarshal(rawTx, &coinbaseTx) 909 | transaction.Metadata, transaction.Operations = ParseCoinbaseTx(coinbaseTx, status, txType) 910 | case SlashTx: 911 | slashTx := ttypes.SlashTx{} 912 | json.Unmarshal(rawTx, &slashTx) 913 | transaction.Metadata, transaction.Operations = ParseSlashTx(slashTx, status, txType) 914 | case SendTx: 915 | sendTx := ttypes.SendTx{} 916 | json.Unmarshal(rawTx, &sendTx) 917 | transaction.Metadata, transaction.Operations = ParseSendTx(sendTx, status, txType) 918 | case ReserveFundTx: 919 | reserveFundTx := ttypes.ReserveFundTx{} 920 | json.Unmarshal(rawTx, &reserveFundTx) 921 | transaction.Metadata, transaction.Operations = ParseReserveFundTx(reserveFundTx, status, txType) 922 | case ReleaseFundTx: 923 | releaseFundTx := ttypes.ReleaseFundTx{} 924 | json.Unmarshal(rawTx, &releaseFundTx) 925 | transaction.Metadata, transaction.Operations = ParseReleaseFundTx(releaseFundTx, status, txType) 926 | case ServicePaymentTx: 927 | servicePaymentTx := ttypes.ServicePaymentTx{} 928 | json.Unmarshal(rawTx, &servicePaymentTx) 929 | transaction.Metadata, transaction.Operations = ParseServicePaymentTx(servicePaymentTx, status, txType) 930 | case SplitRuleTx: 931 | splitRuleTx := ttypes.SplitRuleTx{} 932 | json.Unmarshal(rawTx, &splitRuleTx) 933 | transaction.Metadata, transaction.Operations = ParseSplitRuleTx(splitRuleTx, status, txType) 934 | case SmartContractTx: 935 | smartContractTx := ttypes.SmartContractTx{} 936 | json.Unmarshal(rawTx, &smartContractTx) 937 | transaction.Metadata, transaction.Operations = ParseSmartContractTx(smartContractTx, status, txType, gasUsed, balanceChanges) 938 | case DepositStakeTx, DepositStakeV2Tx: 939 | depositStakeTx := ttypes.DepositStakeTxV2{} 940 | json.Unmarshal(rawTx, &depositStakeTx) 941 | transaction.Metadata, transaction.Operations = ParseDepositStakeTx(depositStakeTx, status, txType) 942 | case WithdrawStakeTx: 943 | withdrawStakeTx := ttypes.WithdrawStakeTx{} 944 | json.Unmarshal(rawTx, &withdrawStakeTx) 945 | transaction.Metadata, transaction.Operations = ParseWithdrawStakeTx(withdrawStakeTx, status, txType) 946 | 947 | if db != nil && stakeService != nil { 948 | // Stakes are returned 28800 blocks later, so store tx in db for later processing 949 | // Query gcp/vcp/eenp to get real withdraw amount 950 | stake, err := stakeService.GetStakeForTx(withdrawStakeTx, blockHeight) 951 | if err == nil { 952 | if withdrawStakeTx.Purpose == core.StakeForValidator || withdrawStakeTx.Purpose == core.StakeForGuardian { 953 | withdrawStakeTx.Source.Coins = ttypes.Coins{ThetaWei: stake.Amount} 954 | } else { 955 | withdrawStakeTx.Source.Coins = ttypes.Coins{TFuelWei: stake.Amount} 956 | } 957 | 958 | returnStakeTx := &ReturnStakeTx{Hash: txHash.Hex(), Tx: withdrawStakeTx} 959 | 960 | returnStakeTxs := ReturnStakeTxs{} 961 | kvstore := NewKVStore(db) 962 | heightBytes := new(big.Int).SetUint64(uint64(blockHeight) + core.ReturnLockingPeriod).Bytes() 963 | if kvstore.Get(heightBytes, &returnStakeTxs) == nil { 964 | exists := false 965 | for _, stake := range returnStakeTxs.ReturnStakes { 966 | if returnStakeTx.Hash == stake.Hash { 967 | exists = true 968 | } 969 | } 970 | if !exists { 971 | returnStakeTxs.ReturnStakes = append(returnStakeTxs.ReturnStakes, returnStakeTx) 972 | } 973 | } else { 974 | returnStakeTxs.ReturnStakes = []*ReturnStakeTx{returnStakeTx} 975 | } 976 | err = kvstore.Put(heightBytes, returnStakeTxs) 977 | if err != nil { 978 | str := fmt.Sprintf("Failed to put stakes for %s at %d, err: %v", txHash.Hex(), heightBytes, err) 979 | panic(str) 980 | } 981 | } else { 982 | errorString := fmt.Sprintf("Failed to get stakes for %s at %d", txHash.Hex(), blockHeight) 983 | panic(errorString) 984 | } 985 | } else { 986 | panic("return_stakes db or service is nil") 987 | } 988 | 989 | transaction.TransactionIdentifier.Hash = crypto.Keccak256Hash([]byte(fmt.Sprintf("%s_%s", StakeWithdrawPrefix, txHash.Hex()))).Hex() 990 | case StakeRewardDistributionTx: 991 | stakeRewardDistributionTx := ttypes.StakeRewardDistributionTx{} 992 | json.Unmarshal(rawTx, &stakeRewardDistributionTx) 993 | transaction.Metadata, transaction.Operations = ParseStakeRewardDistributionTx(stakeRewardDistributionTx, status, txType) 994 | } 995 | 996 | return transaction 997 | } 998 | 999 | func AssembleTx(ops []*types.Operation, meta map[string]interface{}) (tx ttypes.Tx, err error) { 1000 | var ok bool 1001 | var typ, seq interface{} 1002 | if typ, ok = meta["type"]; !ok { 1003 | return nil, fmt.Errorf("missing tx type") 1004 | } 1005 | if seq, ok = meta["sequence"]; !ok { 1006 | return nil, fmt.Errorf("missing tx sequence") 1007 | } 1008 | 1009 | txType := TxType(typ.(float64)) 1010 | sequence := uint64(seq.(float64)) 1011 | fee := ttypes.Coins{TFuelWei: big.NewInt(int64(meta["fee"].(float64)))} 1012 | 1013 | switch txType { 1014 | case CoinbaseTx: 1015 | inputAmount := new(big.Int) 1016 | inputAmount.SetString(ops[0].Amount.Value, 10) 1017 | 1018 | outputs := []ttypes.TxOutput{} 1019 | for i := 1; i < len(ops); i++ { 1020 | tFuelWei := new(big.Int) 1021 | tFuelWei.SetString(ops[i].Amount.Value, 10) 1022 | 1023 | output := ttypes.TxOutput{ 1024 | Address: cmn.HexToAddress(ops[i].Account.Address), 1025 | Coins: ttypes.Coins{TFuelWei: tFuelWei}, 1026 | } 1027 | outputs = append(outputs, output) 1028 | } 1029 | 1030 | tx = &ttypes.CoinbaseTx{ 1031 | Proposer: ttypes.TxInput{ 1032 | Address: cmn.HexToAddress(ops[0].Account.Address), 1033 | Coins: ttypes.Coins{TFuelWei: inputAmount}, 1034 | Sequence: sequence, 1035 | }, 1036 | Outputs: outputs, 1037 | } 1038 | 1039 | case SlashTx: 1040 | inputAmount := new(big.Int) 1041 | inputAmount.SetString(ops[0].Amount.Value, 10) 1042 | 1043 | tx = &ttypes.SlashTx{ 1044 | Proposer: ttypes.TxInput{ 1045 | Address: cmn.HexToAddress(ops[0].Account.Address), 1046 | Coins: ttypes.Coins{TFuelWei: inputAmount}, 1047 | Sequence: sequence, 1048 | }, 1049 | } 1050 | case SendTx: 1051 | var inputs []ttypes.TxInput 1052 | var outputs []ttypes.TxOutput 1053 | 1054 | inputMap := make(map[string][]*types.Operation) 1055 | outputMap := make(map[string][]*types.Operation) 1056 | for _, op := range ops { 1057 | if op.Type == SendTxInput.String() { 1058 | inputMap[op.Account.Address] = append(inputMap[op.Account.Address], op) 1059 | } else if op.Type == SendTxOutput.String() { 1060 | outputMap[op.Account.Address] = append(outputMap[op.Account.Address], op) 1061 | } 1062 | } 1063 | 1064 | for addr, ops := range inputMap { 1065 | input := ttypes.TxInput{ 1066 | Address: cmn.HexToAddress(addr), 1067 | Sequence: sequence, 1068 | } 1069 | for _, op := range ops { 1070 | coin := new(big.Int) 1071 | coin.SetString(op.Amount.Value, 10) 1072 | if strings.EqualFold(op.Amount.Currency.Symbol, GetThetaCurrency().Symbol) { 1073 | input.Coins.ThetaWei = coin 1074 | } else if strings.EqualFold(op.Amount.Currency.Symbol, GetTFuelCurrency().Symbol) { 1075 | input.Coins.TFuelWei = coin 1076 | } 1077 | } 1078 | inputs = append(inputs, input) 1079 | } 1080 | for addr, ops := range outputMap { 1081 | output := ttypes.TxOutput{ 1082 | Address: cmn.HexToAddress(addr), 1083 | } 1084 | for _, op := range ops { 1085 | coin := new(big.Int) 1086 | coin.SetString(op.Amount.Value, 10) 1087 | if strings.EqualFold(op.Amount.Currency.Symbol, GetThetaCurrency().Symbol) { 1088 | output.Coins.ThetaWei = coin 1089 | } else if strings.EqualFold(op.Amount.Currency.Symbol, GetTFuelCurrency().Symbol) { 1090 | output.Coins.TFuelWei = coin 1091 | } 1092 | } 1093 | outputs = append(outputs, output) 1094 | } 1095 | 1096 | tx = &ttypes.SendTx{ 1097 | Fee: fee, 1098 | Inputs: inputs, 1099 | Outputs: outputs, 1100 | } 1101 | 1102 | case ReserveFundTx: 1103 | inputAmount := new(big.Int) 1104 | inputAmount.SetString(ops[0].Amount.Value, 10) 1105 | 1106 | tx = &ttypes.ReserveFundTx{ 1107 | Fee: fee, 1108 | Source: ttypes.TxInput{ 1109 | Address: cmn.HexToAddress(ops[0].Account.Address), 1110 | Coins: ttypes.Coins{TFuelWei: inputAmount}, 1111 | Sequence: sequence, 1112 | }, 1113 | Collateral: meta["collateral"].(ttypes.Coins), 1114 | ResourceIDs: meta["resource_ids"].([]string), 1115 | Duration: uint64(meta["duration"].(float64)), 1116 | } 1117 | 1118 | case ReleaseFundTx: 1119 | inputAmount := new(big.Int) 1120 | inputAmount.SetString(ops[0].Amount.Value, 10) 1121 | 1122 | tx = &ttypes.ReleaseFundTx{ 1123 | Fee: fee, 1124 | Source: ttypes.TxInput{ 1125 | Address: cmn.HexToAddress(ops[0].Account.Address), 1126 | Coins: ttypes.Coins{TFuelWei: inputAmount}, 1127 | Sequence: sequence, 1128 | }, 1129 | ReserveSequence: uint64(meta["reserve_sequence"].(float64)), 1130 | } 1131 | 1132 | case ServicePaymentTx: 1133 | sourceAmount := new(big.Int) 1134 | sourceAmount.SetString(ops[0].Amount.Value, 10) 1135 | targetAmount := new(big.Int) 1136 | targetAmount.SetString(ops[1].Amount.Value, 10) 1137 | 1138 | tx = &ttypes.ServicePaymentTx{ 1139 | Fee: fee, 1140 | Source: ttypes.TxInput{ 1141 | Address: cmn.HexToAddress(ops[0].Account.Address), 1142 | Coins: ttypes.Coins{TFuelWei: sourceAmount}, 1143 | Sequence: sequence, 1144 | }, 1145 | Target: ttypes.TxInput{ 1146 | Address: cmn.HexToAddress(ops[1].Account.Address), 1147 | Coins: ttypes.Coins{TFuelWei: targetAmount}, 1148 | }, 1149 | PaymentSequence: uint64(meta["payment_sequence"].(float64)), 1150 | ReserveSequence: uint64(meta["reserve_sequence"].(float64)), 1151 | ResourceID: meta["resource_id"].(string), 1152 | } 1153 | 1154 | case SplitRuleTx: 1155 | sourceAmount := new(big.Int) 1156 | sourceAmount.SetString(ops[0].Amount.Value, 10) 1157 | 1158 | tx = &ttypes.SplitRuleTx{ 1159 | Fee: fee, 1160 | Initiator: ttypes.TxInput{ 1161 | Address: cmn.HexToAddress(ops[0].Account.Address), 1162 | Coins: ttypes.Coins{TFuelWei: sourceAmount}, 1163 | Sequence: sequence, 1164 | }, 1165 | ResourceID: meta["resource_id"].(string), 1166 | Splits: meta["splits"].([]ttypes.Split), 1167 | Duration: uint64(meta["duration"].(float64)), 1168 | } 1169 | 1170 | case SmartContractTx: 1171 | fromTFuelWei := new(big.Int) 1172 | fromTFuelWei.SetString(ops[0].Amount.Value, 10) 1173 | toTFuelWei := new(big.Int) 1174 | toTFuelWei.SetString(ops[1].Amount.Value, 10) 1175 | 1176 | tx = &ttypes.SmartContractTx{ 1177 | From: ttypes.TxInput{ 1178 | Address: cmn.HexToAddress(ops[0].Account.Address), 1179 | Coins: ttypes.Coins{ThetaWei: big.NewInt(0), TFuelWei: fromTFuelWei}, 1180 | Sequence: sequence, 1181 | }, 1182 | To: ttypes.TxOutput{ 1183 | Address: cmn.HexToAddress(ops[1].Account.Address), 1184 | Coins: ttypes.Coins{ThetaWei: big.NewInt(0), TFuelWei: toTFuelWei}, 1185 | }, 1186 | GasLimit: uint64(meta["gas_limit"].(float64)), 1187 | GasPrice: big.NewInt(int64(meta["gas_price"].(float64))), //new(big.Int).Set(meta["gas_price"].(*big.Int)), 1188 | Data: meta["data"].([]byte), 1189 | } 1190 | 1191 | case DepositStakeTx, DepositStakeV2Tx: 1192 | sourceThetaWei := new(big.Int) 1193 | sourceThetaWei.SetString(ops[0].Amount.Value, 10) 1194 | sourceTFuelWei := new(big.Int) 1195 | sourceTFuelWei.SetString(ops[1].Amount.Value, 10) 1196 | holderThetaWei := new(big.Int) 1197 | holderThetaWei.SetString(ops[2].Amount.Value, 10) 1198 | holderTFuelWei := new(big.Int) 1199 | holderTFuelWei.SetString(ops[3].Amount.Value, 10) 1200 | 1201 | depositStakeTx := &ttypes.DepositStakeTxV2{ 1202 | Fee: fee, 1203 | Purpose: uint8(meta["purpose"].(float64)), 1204 | Source: ttypes.TxInput{ 1205 | Address: cmn.HexToAddress(ops[0].Account.Address), 1206 | Coins: ttypes.Coins{ThetaWei: sourceThetaWei, TFuelWei: sourceTFuelWei}, 1207 | Sequence: sequence, 1208 | }, 1209 | Holder: ttypes.TxOutput{ 1210 | Address: cmn.HexToAddress(ops[1].Account.Address), 1211 | Coins: ttypes.Coins{ThetaWei: holderThetaWei, TFuelWei: holderTFuelWei}, 1212 | }, 1213 | } 1214 | 1215 | if blsPubkey, ok := meta["bls_pub_key"]; ok { 1216 | depositStakeTx.BlsPubkey = blsPubkey.(*bls.PublicKey) 1217 | } 1218 | if blsPop, ok := meta["bls_pop"]; ok { 1219 | depositStakeTx.BlsPop = blsPop.(*bls.Signature) 1220 | } 1221 | if holderSig, ok := meta["holder_sig"]; ok { 1222 | depositStakeTx.HolderSig = holderSig.(*crypto.Signature) 1223 | } 1224 | tx = depositStakeTx 1225 | 1226 | case WithdrawStakeTx: 1227 | sourceThetaWei := new(big.Int) 1228 | sourceThetaWei.SetString(ops[0].Amount.Value, 10) 1229 | sourceTFuelWei := new(big.Int) 1230 | sourceTFuelWei.SetString(ops[1].Amount.Value, 10) 1231 | holderThetaWei := new(big.Int) 1232 | holderThetaWei.SetString(ops[2].Amount.Value, 10) 1233 | holderTFuelWei := new(big.Int) 1234 | holderTFuelWei.SetString(ops[3].Amount.Value, 10) 1235 | 1236 | tx = &ttypes.WithdrawStakeTx{ 1237 | Fee: fee, 1238 | Purpose: uint8(meta["purpose"].(float64)), 1239 | Source: ttypes.TxInput{ 1240 | Address: cmn.HexToAddress(ops[0].Account.Address), 1241 | Coins: ttypes.Coins{ThetaWei: sourceThetaWei, TFuelWei: sourceTFuelWei}, 1242 | Sequence: sequence, 1243 | }, 1244 | Holder: ttypes.TxOutput{ 1245 | Address: cmn.HexToAddress(ops[1].Account.Address), 1246 | Coins: ttypes.Coins{ThetaWei: holderThetaWei, TFuelWei: holderTFuelWei}, 1247 | }, 1248 | } 1249 | 1250 | case StakeRewardDistributionTx: 1251 | holderThetaWei := new(big.Int) 1252 | holderThetaWei.SetString(ops[0].Amount.Value, 10) 1253 | holderTFuelWei := new(big.Int) 1254 | holderTFuelWei.SetString(ops[1].Amount.Value, 10) 1255 | beneficiaryThetaWei := new(big.Int) 1256 | beneficiaryThetaWei.SetString(ops[2].Amount.Value, 10) 1257 | beneficiaryTFuelWei := new(big.Int) 1258 | beneficiaryTFuelWei.SetString(ops[3].Amount.Value, 10) 1259 | 1260 | tx = &ttypes.StakeRewardDistributionTx{ 1261 | Fee: fee, 1262 | SplitBasisPoint: uint(meta["split_basis_point"].(float64)), 1263 | Holder: ttypes.TxInput{ 1264 | Address: cmn.HexToAddress(ops[0].Account.Address), 1265 | Coins: ttypes.Coins{ThetaWei: holderThetaWei, TFuelWei: holderTFuelWei}, 1266 | Sequence: sequence, 1267 | }, 1268 | Beneficiary: ttypes.TxOutput{ 1269 | Address: cmn.HexToAddress(ops[1].Account.Address), 1270 | Coins: ttypes.Coins{ThetaWei: beneficiaryThetaWei, TFuelWei: beneficiaryTFuelWei}, 1271 | }, 1272 | } 1273 | 1274 | default: 1275 | return nil, fmt.Errorf("unsupported tx type") 1276 | } 1277 | return 1278 | } 1279 | --------------------------------------------------------------------------------