From dbd09b97b45fb3ae672ae676a5cb94c754db2e07 Mon Sep 17 00:00:00 2001 From: nnhuyhoang Date: Tue, 19 Nov 2024 23:59:03 +0700 Subject: [PATCH 1/3] feat: add telementry package --- go.mod | 1 + go.sum | 2 ++ internal/model/onchain_btc_transaction.go | 14 ++++++++++ internal/model/onchain_icy_transaction.go | 21 +++++++++++++++ internal/server/server.go | 11 ++++++++ .../store/onchainbtctransaction/interface.go | 10 +++++++ .../onchain_btc_transaction.go | 16 ++++++++++++ .../store/onchainicytransaction/interface.go | 10 +++++++ .../onchain_icy_transaction.go | 16 ++++++++++++ internal/store/store.go | 14 +++++++--- internal/telemetry/interface.go | 4 +++ internal/telemetry/telemetry.go | 18 +++++++++++++ ...02_add_onchain_transaction_tables.down.sql | 3 +++ ...0002_add_onchain_transaction_tables.up.sql | 26 +++++++++++++++++++ 14 files changed, 163 insertions(+), 3 deletions(-) create mode 100644 internal/model/onchain_btc_transaction.go create mode 100644 internal/model/onchain_icy_transaction.go create mode 100644 internal/store/onchainbtctransaction/interface.go create mode 100644 internal/store/onchainbtctransaction/onchain_btc_transaction.go create mode 100644 internal/store/onchainicytransaction/interface.go create mode 100644 internal/store/onchainicytransaction/onchain_icy_transaction.go create mode 100644 internal/telemetry/interface.go create mode 100644 internal/telemetry/telemetry.go create mode 100644 migrations/schema/0002_add_onchain_transaction_tables.down.sql create mode 100644 migrations/schema/0002_add_onchain_transaction_tables.up.sql diff --git a/go.mod b/go.mod index 3df3215..5a2ec40 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/joho/godotenv v1.5.1 github.com/onsi/ginkgo/v2 v2.21.0 github.com/onsi/gomega v1.35.1 + github.com/robfig/cron/v3 v3.0.1 github.com/stretchr/testify v1.9.0 github.com/swaggo/files v1.0.1 github.com/swaggo/gin-swagger v1.6.0 diff --git a/go.sum b/go.sum index 3327c33..807942b 100644 --- a/go.sum +++ b/go.sum @@ -287,6 +287,8 @@ github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0 github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= diff --git a/internal/model/onchain_btc_transaction.go b/internal/model/onchain_btc_transaction.go new file mode 100644 index 0000000..5dca2af --- /dev/null +++ b/internal/model/onchain_btc_transaction.go @@ -0,0 +1,14 @@ +package model + +type OnchainBtcTransaction struct { + ID int `json:"id"` + InternalID string `json:"internal_id"` + TransactionHash string `json:"transaction_hash"` + TransactionTimestamp int64 `json:"transaction_timestamp"` + Type TransactionType `json:"type"` + Amount string `json:"amount"` + SenderAddress string `json:"sender_address"` + ReceiverAddress string `json:"receiver_address"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` +} diff --git a/internal/model/onchain_icy_transaction.go b/internal/model/onchain_icy_transaction.go new file mode 100644 index 0000000..5ab816d --- /dev/null +++ b/internal/model/onchain_icy_transaction.go @@ -0,0 +1,21 @@ +package model + +type TransactionType string + +const ( + Out TransactionType = "out" + In TransactionType = "in" +) + +type OnchainIcyTransaction struct { + ID int `json:"id"` + InternalID string `json:"internal_id"` + TransactionHash string `json:"transaction_hash"` + TransactionTimestamp int64 `json:"transaction_timestamp"` + Type TransactionType `json:"type"` + Amount string `json:"amount"` + SenderAddress string `json:"sender_address"` + ReceiverAddress string `json:"receiver_address"` + CreatedAt int64 `json:"created_at"` + UpdatedAt int64 `json:"updated_at"` +} diff --git a/internal/server/server.go b/internal/server/server.go index ed4621e..521fd56 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -6,9 +6,11 @@ import ( "github.com/dwarvesf/icy-backend/internal/oracle" "github.com/dwarvesf/icy-backend/internal/store" pgstore "github.com/dwarvesf/icy-backend/internal/store/postgres" + "github.com/dwarvesf/icy-backend/internal/telemetry" "github.com/dwarvesf/icy-backend/internal/transport/http" "github.com/dwarvesf/icy-backend/internal/utils/config" "github.com/dwarvesf/icy-backend/internal/utils/logger" + "github.com/robfig/cron/v3" ) func Init() { @@ -26,6 +28,15 @@ func Init() { } oracle := oracle.New(db, s, appConfig, logger, btcRpc, baseRpc) + _ = telemetry.New(appConfig, logger) + + c := cron.New() + + c.AddFunc("@every 2m", func() { + //TODO: + // telemetry.UpdateData() + }) + httpServer := http.NewHttpServer(appConfig, logger, oracle) httpServer.Run() diff --git a/internal/store/onchainbtctransaction/interface.go b/internal/store/onchainbtctransaction/interface.go new file mode 100644 index 0000000..7cbf571 --- /dev/null +++ b/internal/store/onchainbtctransaction/interface.go @@ -0,0 +1,10 @@ +package onchainbtctransaction + +import ( + "github.com/dwarvesf/icy-backend/internal/model" + "gorm.io/gorm" +) + +type IStore interface { + Create(db *gorm.DB, onchainBtcTransaction *model.OnchainBtcTransaction) (*model.OnchainBtcTransaction, error) +} diff --git a/internal/store/onchainbtctransaction/onchain_btc_transaction.go b/internal/store/onchainbtctransaction/onchain_btc_transaction.go new file mode 100644 index 0000000..e30967d --- /dev/null +++ b/internal/store/onchainbtctransaction/onchain_btc_transaction.go @@ -0,0 +1,16 @@ +package onchainbtctransaction + +import ( + "github.com/dwarvesf/icy-backend/internal/model" + "gorm.io/gorm" +) + +type store struct{} + +func New() IStore { + return &store{} +} + +func (s *store) Create(db *gorm.DB, onchainBtcTransaction *model.OnchainBtcTransaction) (*model.OnchainBtcTransaction, error) { + return onchainBtcTransaction, db.Create(onchainBtcTransaction).Error +} diff --git a/internal/store/onchainicytransaction/interface.go b/internal/store/onchainicytransaction/interface.go new file mode 100644 index 0000000..b45a519 --- /dev/null +++ b/internal/store/onchainicytransaction/interface.go @@ -0,0 +1,10 @@ +package onchainicytransaction + +import ( + "github.com/dwarvesf/icy-backend/internal/model" + "gorm.io/gorm" +) + +type IStore interface { + Create(db *gorm.DB, onchainIcyTransaction *model.OnchainIcyTransaction) (*model.OnchainIcyTransaction, error) +} diff --git a/internal/store/onchainicytransaction/onchain_icy_transaction.go b/internal/store/onchainicytransaction/onchain_icy_transaction.go new file mode 100644 index 0000000..24e369f --- /dev/null +++ b/internal/store/onchainicytransaction/onchain_icy_transaction.go @@ -0,0 +1,16 @@ +package onchainicytransaction + +import ( + "github.com/dwarvesf/icy-backend/internal/model" + "gorm.io/gorm" +) + +type store struct{} + +func New() IStore { + return &store{} +} + +func (s *store) Create(db *gorm.DB, onchainIcyTransaction *model.OnchainIcyTransaction) (*model.OnchainIcyTransaction, error) { + return onchainIcyTransaction, db.Create(onchainIcyTransaction).Error +} diff --git a/internal/store/store.go b/internal/store/store.go index 5f3ec5e..b8db0e1 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -1,13 +1,21 @@ package store -import "github.com/dwarvesf/icy-backend/internal/store/icylockedtreasury" +import ( + "github.com/dwarvesf/icy-backend/internal/store/icylockedtreasury" + "github.com/dwarvesf/icy-backend/internal/store/onchainbtctransaction" + "github.com/dwarvesf/icy-backend/internal/store/onchainicytransaction" +) type Store struct { - IcyLockedTreasury icylockedtreasury.IStore + IcyLockedTreasury icylockedtreasury.IStore + OnchainBtcTransaction onchainbtctransaction.IStore + OnchainIcyTransaction onchainicytransaction.IStore } func New() *Store { return &Store{ - IcyLockedTreasury: icylockedtreasury.New(), + IcyLockedTreasury: icylockedtreasury.New(), + OnchainBtcTransaction: onchainbtctransaction.New(), + OnchainIcyTransaction: onchainicytransaction.New(), } } diff --git a/internal/telemetry/interface.go b/internal/telemetry/interface.go new file mode 100644 index 0000000..fc6adb0 --- /dev/null +++ b/internal/telemetry/interface.go @@ -0,0 +1,4 @@ +package telemetry + +type ITelemetry interface { +} diff --git a/internal/telemetry/telemetry.go b/internal/telemetry/telemetry.go new file mode 100644 index 0000000..e6ec402 --- /dev/null +++ b/internal/telemetry/telemetry.go @@ -0,0 +1,18 @@ +package telemetry + +import ( + "github.com/dwarvesf/icy-backend/internal/utils/config" + "github.com/dwarvesf/icy-backend/internal/utils/logger" +) + +type Telemetry struct { + appConfig *config.AppConfig + logger *logger.Logger +} + +func New(appConfig *config.AppConfig, logger *logger.Logger) *Telemetry { + return &Telemetry{ + appConfig: appConfig, + logger: logger, + } +} diff --git a/migrations/schema/0002_add_onchain_transaction_tables.down.sql b/migrations/schema/0002_add_onchain_transaction_tables.down.sql new file mode 100644 index 0000000..12fb340 --- /dev/null +++ b/migrations/schema/0002_add_onchain_transaction_tables.down.sql @@ -0,0 +1,3 @@ +DROP TABLE IF EXISTS "onchain_icy_transactions"; + +DROP TABLE IF EXISTS "onchain_btc_transactions"; \ No newline at end of file diff --git a/migrations/schema/0002_add_onchain_transaction_tables.up.sql b/migrations/schema/0002_add_onchain_transaction_tables.up.sql new file mode 100644 index 0000000..58c676b --- /dev/null +++ b/migrations/schema/0002_add_onchain_transaction_tables.up.sql @@ -0,0 +1,26 @@ + +CREATE TABLE "onchain_icy_transactions" ( + "id" SERIAL PRIMARY KEY, + "internal_id" VARCHAR NOT NULL, + "transaction_hash" VARCHAR NOT NULL, + "transaction_timestamp" TIMESTAMP NOT NULL, + "type" VARCHAR NOT NULL, + "amount" VARCHAR NOT NULL, + "sender_address" VARCHAR NOT NULL, + "receiver_address" VARCHAR NOT NULL, + "created_at" TIMESTAMP NOT NULL DEFAULT now(), + "updated_at" TIMESTAMP NOT NULL DEFAULT now() +); + +CREATE TABLE "onchain_btc_transactions" ( + "id" SERIAL PRIMARY KEY, + "internal_id" VARCHAR NOT NULL, + "transaction_hash" VARCHAR NOT NULL, + "transaction_timestamp" TIMESTAMP NOT NULL, + "type" VARCHAR NOT NULL, + "amount" VARCHAR NOT NULL, + "sender_address" VARCHAR NOT NULL, + "receiver_address" VARCHAR NOT NULL, + "created_at" TIMESTAMP NOT NULL DEFAULT now(), + "updated_at" TIMESTAMP NOT NULL DEFAULT now() +); From be060ba4f2770109e5acab2040c81ddf62d1eea7 Mon Sep 17 00:00:00 2001 From: nnhuyhoang Date: Wed, 20 Nov 2024 17:35:23 +0700 Subject: [PATCH 2/3] feat: add telementry package --- devbox.lock | 274 ------------------ internal/btcrpc/blockstream/blockstream.go | 48 ++- internal/btcrpc/blockstream/interface.go | 1 + internal/btcrpc/blockstream/types.go | 42 +++ internal/btcrpc/btcrpc.go | 75 ++++- internal/btcrpc/interface.go | 1 + internal/model/onchain_btc_transaction.go | 22 +- internal/model/onchain_icy_transaction.go | 22 +- internal/server/server.go | 7 +- .../store/onchainbtctransaction/interface.go | 1 + .../onchain_btc_transaction.go | 5 + internal/telemetry/interface.go | 1 + internal/telemetry/telemetry.go | 59 +++- ...0002_add_onchain_transaction_tables.up.sql | 12 +- 14 files changed, 262 insertions(+), 308 deletions(-) delete mode 100644 devbox.lock diff --git a/devbox.lock b/devbox.lock deleted file mode 100644 index 8ecbeed..0000000 --- a/devbox.lock +++ /dev/null @@ -1,274 +0,0 @@ -{ - "lockfile_version": "1", - "packages": { - "go-migrate@latest": { - "last_modified": "2024-11-03T14:18:04Z", - "resolved": "github:NixOS/nixpkgs/4ae2e647537bcdbb82265469442713d066675275#go-migrate", - "source": "devbox-search", - "version": "4.18.1", - "systems": { - "aarch64-darwin": { - "outputs": [ - { - "name": "out", - "path": "/nix/store/hwnqa1iwn3pxvzsrzjdshxc9kjnipzx3-go-migrate-4.18.1", - "default": true - } - ], - "store_path": "/nix/store/hwnqa1iwn3pxvzsrzjdshxc9kjnipzx3-go-migrate-4.18.1" - }, - "aarch64-linux": { - "outputs": [ - { - "name": "out", - "path": "/nix/store/wq0zb6rnzfjmks5hnpsg1vapzq8y00db-go-migrate-4.18.1", - "default": true - } - ], - "store_path": "/nix/store/wq0zb6rnzfjmks5hnpsg1vapzq8y00db-go-migrate-4.18.1" - }, - "x86_64-darwin": { - "outputs": [ - { - "name": "out", - "path": "/nix/store/r820hli73x34ls77frvvpz8xdpq2kcvi-go-migrate-4.18.1", - "default": true - } - ], - "store_path": "/nix/store/r820hli73x34ls77frvvpz8xdpq2kcvi-go-migrate-4.18.1" - }, - "x86_64-linux": { - "outputs": [ - { - "name": "out", - "path": "/nix/store/dlma2w3v0rr8fis8rvy4wrlg9chcalb7-go-migrate-4.18.1", - "default": true - } - ], - "store_path": "/nix/store/dlma2w3v0rr8fis8rvy4wrlg9chcalb7-go-migrate-4.18.1" - } - } - }, - "go-swag@1.8.12": { - "last_modified": "2024-03-08T13:51:52Z", - "resolved": "github:NixOS/nixpkgs/a343533bccc62400e8a9560423486a3b6c11a23b#go-swag", - "source": "devbox-search", - "version": "1.8.12", - "systems": { - "aarch64-darwin": { - "outputs": [ - { - "name": "out", - "path": "/nix/store/sdl346zvps3hm9r2zbxm5sxqydzfbr3h-go-swag-1.8.12", - "default": true - } - ], - "store_path": "/nix/store/sdl346zvps3hm9r2zbxm5sxqydzfbr3h-go-swag-1.8.12" - }, - "aarch64-linux": { - "outputs": [ - { - "name": "out", - "path": "/nix/store/1m1aa9mhfkw8z55h0m7mfv06mn04m65s-go-swag-1.8.12", - "default": true - } - ], - "store_path": "/nix/store/1m1aa9mhfkw8z55h0m7mfv06mn04m65s-go-swag-1.8.12" - }, - "x86_64-darwin": { - "outputs": [ - { - "name": "out", - "path": "/nix/store/dqzrhw11y2vwkar2kwsbhgz483zchy05-go-swag-1.8.12", - "default": true - } - ], - "store_path": "/nix/store/dqzrhw11y2vwkar2kwsbhgz483zchy05-go-swag-1.8.12" - }, - "x86_64-linux": { - "outputs": [ - { - "name": "out", - "path": "/nix/store/zyi1wnwd54m1ysj3l6lqj2ndi9h17k0k-go-swag-1.8.12", - "default": true - } - ], - "store_path": "/nix/store/zyi1wnwd54m1ysj3l6lqj2ndi9h17k0k-go-swag-1.8.12" - } - } - }, - "go@1.23.0": { - "last_modified": "2024-08-31T10:12:23Z", - "resolved": "github:NixOS/nixpkgs/5629520edecb69630a3f4d17d3d33fc96c13f6fe#go_1_23", - "source": "devbox-search", - "version": "1.23.0", - "systems": { - "aarch64-darwin": { - "outputs": [ - { - "name": "out", - "path": "/nix/store/4cijk6gwv59c84h1l9yhxzsaz93f67mz-go-1.23.0", - "default": true - } - ], - "store_path": "/nix/store/4cijk6gwv59c84h1l9yhxzsaz93f67mz-go-1.23.0" - }, - "aarch64-linux": { - "outputs": [ - { - "name": "out", - "path": "/nix/store/mhwsyzk9v43q67ic34c02sxnsnbj7qbh-go-1.23.0", - "default": true - } - ], - "store_path": "/nix/store/mhwsyzk9v43q67ic34c02sxnsnbj7qbh-go-1.23.0" - }, - "x86_64-darwin": { - "outputs": [ - { - "name": "out", - "path": "/nix/store/vbcqda38ha9gqsbwjw4q0swpwlvmnb1i-go-1.23.0", - "default": true - } - ], - "store_path": "/nix/store/vbcqda38ha9gqsbwjw4q0swpwlvmnb1i-go-1.23.0" - }, - "x86_64-linux": { - "outputs": [ - { - "name": "out", - "path": "/nix/store/h5wkf711ql98c59n7yxa146jbjf9vrj5-go-1.23.0", - "default": true - } - ], - "store_path": "/nix/store/h5wkf711ql98c59n7yxa146jbjf9vrj5-go-1.23.0" - } - } - }, - "postgresql@latest": { - "last_modified": "2024-11-05T18:23:38Z", - "plugin_version": "0.0.2", - "resolved": "github:NixOS/nixpkgs/8c4dc69b9732f6bbe826b5fbb32184987520ff26#postgresql", - "source": "devbox-search", - "version": "16.4", - "systems": { - "aarch64-darwin": { - "outputs": [ - { - "name": "out", - "path": "/nix/store/njzzxcv9qbvpc3as73zynk8i30qlfkyk-postgresql-16.4", - "default": true - }, - { - "name": "man", - "path": "/nix/store/vm90nyq3v42gka36vfxgyvbsqgavf30h-postgresql-16.4-man", - "default": true - }, - { - "name": "dev", - "path": "/nix/store/7dlym60qc7jmhm8d44f7dsnaghb7xdvk-postgresql-16.4-dev" - }, - { - "name": "doc", - "path": "/nix/store/pqrjg70wwgam8rq48cm1yibk87iy81cg-postgresql-16.4-doc" - }, - { - "name": "lib", - "path": "/nix/store/1n8ppphidl1nprqdnc70jqd9bnnqbhz0-postgresql-16.4-lib" - } - ], - "store_path": "/nix/store/njzzxcv9qbvpc3as73zynk8i30qlfkyk-postgresql-16.4" - }, - "aarch64-linux": { - "outputs": [ - { - "name": "out", - "path": "/nix/store/h0kz668dqw9c3d4vx4c28ikn4b8hgds4-postgresql-16.4", - "default": true - }, - { - "name": "man", - "path": "/nix/store/fmkxhxwjfd8v428vbhzhz7myh2d4jx26-postgresql-16.4-man", - "default": true - }, - { - "name": "debug", - "path": "/nix/store/4b6pzkw3sw3d1xz31ba0h82sk4m9q7ch-postgresql-16.4-debug" - }, - { - "name": "dev", - "path": "/nix/store/nw8xyfklwnp9afjywa89q9v549mdmx9s-postgresql-16.4-dev" - }, - { - "name": "doc", - "path": "/nix/store/w7af6r825fwlgdfr5q8gn906mg5522fb-postgresql-16.4-doc" - }, - { - "name": "lib", - "path": "/nix/store/7kg75r4716xf0pf15igpdcdlczhp981x-postgresql-16.4-lib" - } - ], - "store_path": "/nix/store/h0kz668dqw9c3d4vx4c28ikn4b8hgds4-postgresql-16.4" - }, - "x86_64-darwin": { - "outputs": [ - { - "name": "out", - "path": "/nix/store/8r8zgm795png9iwbc9sv296d4rvc494a-postgresql-16.4", - "default": true - }, - { - "name": "man", - "path": "/nix/store/vrbawdhrrvvliqjr0g53804rqrcxnpfq-postgresql-16.4-man", - "default": true - }, - { - "name": "dev", - "path": "/nix/store/mb1x8id6pjm1j2wvbrpmxbnpaj91hvp8-postgresql-16.4-dev" - }, - { - "name": "doc", - "path": "/nix/store/7n6zyhmk7yki0kzfmbxvjjyrb9qskfv8-postgresql-16.4-doc" - }, - { - "name": "lib", - "path": "/nix/store/vdfbff4iad2nv03dzmgpp8axx5n50xmb-postgresql-16.4-lib" - } - ], - "store_path": "/nix/store/8r8zgm795png9iwbc9sv296d4rvc494a-postgresql-16.4" - }, - "x86_64-linux": { - "outputs": [ - { - "name": "out", - "path": "/nix/store/wc1a06ip2fajrjkfbw7cvxzw1c949a6g-postgresql-16.4", - "default": true - }, - { - "name": "man", - "path": "/nix/store/h2kawzk00cnmqvbp860hispl7rr8dyij-postgresql-16.4-man", - "default": true - }, - { - "name": "debug", - "path": "/nix/store/krb6fp58dkx0v1w6ljgsc1q4fa9px38b-postgresql-16.4-debug" - }, - { - "name": "dev", - "path": "/nix/store/a1y4jday73043sr9jdkl45x86fc4bz6b-postgresql-16.4-dev" - }, - { - "name": "doc", - "path": "/nix/store/aimqn571cc15g9dylir4sg9hddsx9vk4-postgresql-16.4-doc" - }, - { - "name": "lib", - "path": "/nix/store/y44iixsj2fd1pcy8ny66809z8r6zlxag-postgresql-16.4-lib" - } - ], - "store_path": "/nix/store/wc1a06ip2fajrjkfbw7cvxzw1c949a6g-postgresql-16.4" - } - } - } - } -} diff --git a/internal/btcrpc/blockstream/blockstream.go b/internal/btcrpc/blockstream/blockstream.go index 20f9bad..6bc7cba 100644 --- a/internal/btcrpc/blockstream/blockstream.go +++ b/internal/btcrpc/blockstream/blockstream.go @@ -96,7 +96,8 @@ func (c *blockstream) GetUTXOs(address string) ([]UTXO, error) { return utxos, nil } -func (c *blockstream) GetBTCBalance(url string) (*model.Web3BigInt, error) { +func (c *blockstream) GetBTCBalance(address string) (*model.Web3BigInt, error) { + url := fmt.Sprintf("%s/address/%s", c.baseURL, address) var lastErr error maxRetries := 3 @@ -151,3 +152,48 @@ func (c *blockstream) GetBTCBalance(url string) (*model.Web3BigInt, error) { return nil, lastErr } + +func (c *blockstream) GetTransactionsByAddress(address string, fromTxID string) ([]Transaction, error) { + var url string + if fromTxID == "" { + url = fmt.Sprintf("%s/address/%s/txs", c.baseURL, address) + } else { + url = fmt.Sprintf("%s/address/%s/txs/chain/%s", c.baseURL, address, fromTxID) + } + + var lastErr error + maxRetries := 3 + + for attempt := 1; attempt <= maxRetries; attempt++ { + resp, err := c.client.Get(url) + if err != nil { + lastErr = err + c.logger.Error("[GetTransactionsByAddress][client.Get]", map[string]string{ + "error": err.Error(), + "attempt": strconv.Itoa(attempt), + }) + continue + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + lastErr = fmt.Errorf("unexpected status code: %d", resp.StatusCode) + c.logger.Error("[GetTransactionsByAddress][client.Get]", map[string]string{ + "error": "unexpected status code", + "attempt": strconv.Itoa(attempt), + }) + continue + } + + var txs []Transaction + if err := json.NewDecoder(resp.Body).Decode(&txs); err != nil { + lastErr = err + c.logger.Error("[GetTransactionsByAddress][json.NewDecoder.Decode]", map[string]string{ + "error": err.Error(), + }) + continue + } + return txs, nil + } + return nil, lastErr +} diff --git a/internal/btcrpc/blockstream/interface.go b/internal/btcrpc/blockstream/interface.go index 85a129c..9548c3f 100644 --- a/internal/btcrpc/blockstream/interface.go +++ b/internal/btcrpc/blockstream/interface.go @@ -7,4 +7,5 @@ type IBlockStream interface { EstimateFees() (fees map[string]float64, err error) GetUTXOs(address string) ([]UTXO, error) GetBTCBalance(address string) (balance *model.Web3BigInt, err error) + GetTransactionsByAddress(address string, fromTxID string) ([]Transaction, error) } diff --git a/internal/btcrpc/blockstream/types.go b/internal/btcrpc/blockstream/types.go index e6d6a8e..7ed20ed 100644 --- a/internal/btcrpc/blockstream/types.go +++ b/internal/btcrpc/blockstream/types.go @@ -33,3 +33,45 @@ type GetBalanceResponse struct { ChainStats ChainStats `json:"chain_stats"` MempoolStats MempoolStats `json:"mempool_stats"` } + +// Transaction represents a Bitcoin transaction from the Esplora API +type Transaction struct { + TxID string `json:"txid"` + Version int32 `json:"version"` + Locktime uint32 `json:"locktime"` + Size uint32 `json:"size"` + Weight uint32 `json:"weight"` + Fee int64 `json:"fee"` + Vin []Input `json:"vin"` + Vout []Output `json:"vout"` + Status TxStatus `json:"status"` +} + +// Input represents a transaction input +type Input struct { + TxID string `json:"txid"` + Vout uint32 `json:"vout"` + Prevout *Output `json:"prevout"` + ScriptSig string `json:"scriptsig"` + ScriptSigAsm string `json:"scriptsig_asm"` + Witness []string `json:"witness"` + IsCoinbase bool `json:"is_coinbase"` + Sequence uint32 `json:"sequence"` +} + +// Output represents a transaction output +type Output struct { + ScriptPubKey string `json:"scriptpubkey"` + ScriptPubKeyAsm string `json:"scriptpubkey_asm"` + ScriptPubKeyType string `json:"scriptpubkey_type"` + ScriptPubKeyAddress string `json:"scriptpubkey_address"` + Value int64 `json:"value"` +} + +// TxStatus represents the status of a transaction +type TxStatus struct { + Confirmed bool `json:"confirmed"` + BlockHeight uint32 `json:"block_height,omitempty"` + BlockHash string `json:"block_hash,omitempty"` + BlockTime int64 `json:"block_time,omitempty"` +} diff --git a/internal/btcrpc/btcrpc.go b/internal/btcrpc/btcrpc.go index 23a3fd4..6702c0e 100644 --- a/internal/btcrpc/btcrpc.go +++ b/internal/btcrpc/btcrpc.go @@ -2,6 +2,8 @@ package btcrpc import ( "fmt" + "slices" + "strconv" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" @@ -98,9 +100,7 @@ func (b *BtcRpc) Send(receiverAddressStr string, amount *model.Web3BigInt) error } func (b *BtcRpc) CurrentBalance() (*model.Web3BigInt, error) { - url := b.appConfig.Bitcoin.BlockstreamAPIURL + "/address/" + b.appConfig.Blockchain.BTCTreasuryAddress - - balance, err := b.blockstream.GetBTCBalance(url) + balance, err := b.blockstream.GetBTCBalance(b.appConfig.Blockchain.BTCTreasuryAddress) if err != nil { b.logger.Error("[CurrentBalance][GetBTCBalance]", map[string]string{ "error": err.Error(), @@ -110,3 +110,72 @@ func (b *BtcRpc) CurrentBalance() (*model.Web3BigInt, error) { return balance, nil } + +func (b *BtcRpc) GetTransactionsByAddress(address string, fromTxId string) ([]model.OnchainBtcTransaction, error) { + rawTx, err := b.blockstream.GetTransactionsByAddress(address, fromTxId) + if err != nil { + b.logger.Error("[GetTransactionsByAddress][GetTransactionsByAddress]", map[string]string{ + "error": err.Error(), + }) + return nil, err + } + + // Filter out unconfirmed transactions + confirmedTx := make([]blockstream.Transaction, 0) + for _, tx := range rawTx { + if tx.TxID == fromTxId { + break + } + if tx.Status.Confirmed { + confirmedTx = append(confirmedTx, tx) + } + } + + slices.Reverse(confirmedTx) + + transactions := make([]model.OnchainBtcTransaction, 0) + for _, tx := range confirmedTx { + var isOutgoing bool + var senderAddress string + for _, input := range tx.Vin { + prevOut := input.Prevout + if prevOut != nil { + if prevOut.ScriptPubKeyAddress == address { + isOutgoing = true + } else { + senderAddress = prevOut.ScriptPubKeyAddress + } + } + } + + if isOutgoing { + for _, output := range tx.Vout { + if output.ScriptPubKeyAddress != address { + transactions = append(transactions, model.OnchainBtcTransaction{ + TransactionHash: tx.TxID, + Amount: strconv.FormatInt(output.Value, 10), + Type: model.Out, + OtherAddress: output.ScriptPubKeyAddress, + BlockTime: tx.Status.BlockTime, + InternalID: tx.TxID, + Fee: strconv.FormatInt(tx.Fee, 10), + }) + } + } + } else { + for _, output := range tx.Vout { + if output.ScriptPubKeyAddress == address { + transactions = append(transactions, model.OnchainBtcTransaction{ + TransactionHash: tx.TxID, + Amount: strconv.FormatInt(output.Value, 10), + Type: model.In, + OtherAddress: senderAddress, + BlockTime: tx.Status.BlockTime, + InternalID: tx.TxID, + }) + } + } + } + } + return transactions, nil +} diff --git a/internal/btcrpc/interface.go b/internal/btcrpc/interface.go index 04630e6..e044692 100644 --- a/internal/btcrpc/interface.go +++ b/internal/btcrpc/interface.go @@ -5,4 +5,5 @@ import "github.com/dwarvesf/icy-backend/internal/model" type IBtcRpc interface { Send(receiverAddress string, amount *model.Web3BigInt) error CurrentBalance() (*model.Web3BigInt, error) + GetTransactionsByAddress(address string, fromTxId string) ([]model.OnchainBtcTransaction, error) } diff --git a/internal/model/onchain_btc_transaction.go b/internal/model/onchain_btc_transaction.go index 5dca2af..d9acb4e 100644 --- a/internal/model/onchain_btc_transaction.go +++ b/internal/model/onchain_btc_transaction.go @@ -1,14 +1,16 @@ package model +import "time" + type OnchainBtcTransaction struct { - ID int `json:"id"` - InternalID string `json:"internal_id"` - TransactionHash string `json:"transaction_hash"` - TransactionTimestamp int64 `json:"transaction_timestamp"` - Type TransactionType `json:"type"` - Amount string `json:"amount"` - SenderAddress string `json:"sender_address"` - ReceiverAddress string `json:"receiver_address"` - CreatedAt int64 `json:"created_at"` - UpdatedAt int64 `json:"updated_at"` + ID int `json:"id"` + InternalID string `json:"internal_id"` + TransactionHash string `json:"transaction_hash"` + BlockTime int64 `json:"block_time"` + Type TransactionType `json:"type"` + Amount string `json:"amount"` + Fee string `json:"fee"` + OtherAddress string `json:"other_address"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } diff --git a/internal/model/onchain_icy_transaction.go b/internal/model/onchain_icy_transaction.go index 5ab816d..db30bde 100644 --- a/internal/model/onchain_icy_transaction.go +++ b/internal/model/onchain_icy_transaction.go @@ -1,5 +1,7 @@ package model +import "time" + type TransactionType string const ( @@ -8,14 +10,14 @@ const ( ) type OnchainIcyTransaction struct { - ID int `json:"id"` - InternalID string `json:"internal_id"` - TransactionHash string `json:"transaction_hash"` - TransactionTimestamp int64 `json:"transaction_timestamp"` - Type TransactionType `json:"type"` - Amount string `json:"amount"` - SenderAddress string `json:"sender_address"` - ReceiverAddress string `json:"receiver_address"` - CreatedAt int64 `json:"created_at"` - UpdatedAt int64 `json:"updated_at"` + ID int `json:"id"` + InternalID string `json:"internal_id"` + TransactionHash string `json:"transaction_hash"` + BlockTime int64 `json:"block_time"` + Type TransactionType `json:"type"` + Amount string `json:"amount"` + Fee string `json:"fee"` + OtherAddress string `json:"other_address"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } diff --git a/internal/server/server.go b/internal/server/server.go index 521fd56..f7f8e90 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -28,15 +28,16 @@ func Init() { } oracle := oracle.New(db, s, appConfig, logger, btcRpc, baseRpc) - _ = telemetry.New(appConfig, logger) + telemetry := telemetry.New(db, s, appConfig, logger, btcRpc) c := cron.New() c.AddFunc("@every 2m", func() { - //TODO: - // telemetry.UpdateData() + telemetry.IndexBtcTransaction() }) + c.Start() + httpServer := http.NewHttpServer(appConfig, logger, oracle) httpServer.Run() diff --git a/internal/store/onchainbtctransaction/interface.go b/internal/store/onchainbtctransaction/interface.go index 7cbf571..c7ee98e 100644 --- a/internal/store/onchainbtctransaction/interface.go +++ b/internal/store/onchainbtctransaction/interface.go @@ -7,4 +7,5 @@ import ( type IStore interface { Create(db *gorm.DB, onchainBtcTransaction *model.OnchainBtcTransaction) (*model.OnchainBtcTransaction, error) + GetLatestTransaction(db *gorm.DB) (*model.OnchainBtcTransaction, error) } diff --git a/internal/store/onchainbtctransaction/onchain_btc_transaction.go b/internal/store/onchainbtctransaction/onchain_btc_transaction.go index e30967d..1bea324 100644 --- a/internal/store/onchainbtctransaction/onchain_btc_transaction.go +++ b/internal/store/onchainbtctransaction/onchain_btc_transaction.go @@ -14,3 +14,8 @@ func New() IStore { func (s *store) Create(db *gorm.DB, onchainBtcTransaction *model.OnchainBtcTransaction) (*model.OnchainBtcTransaction, error) { return onchainBtcTransaction, db.Create(onchainBtcTransaction).Error } + +func (s *store) GetLatestTransaction(db *gorm.DB) (*model.OnchainBtcTransaction, error) { + var onchainBtcTransaction model.OnchainBtcTransaction + return &onchainBtcTransaction, db.Order("created_at desc").First(&onchainBtcTransaction).Error +} diff --git a/internal/telemetry/interface.go b/internal/telemetry/interface.go index fc6adb0..a5439e2 100644 --- a/internal/telemetry/interface.go +++ b/internal/telemetry/interface.go @@ -1,4 +1,5 @@ package telemetry type ITelemetry interface { + IndexBtcTransaction() error } diff --git a/internal/telemetry/telemetry.go b/internal/telemetry/telemetry.go index e6ec402..f9d8533 100644 --- a/internal/telemetry/telemetry.go +++ b/internal/telemetry/telemetry.go @@ -1,18 +1,75 @@ package telemetry import ( + "errors" + "fmt" + + "github.com/dwarvesf/icy-backend/internal/btcrpc" + "github.com/dwarvesf/icy-backend/internal/model" + "github.com/dwarvesf/icy-backend/internal/store" "github.com/dwarvesf/icy-backend/internal/utils/config" "github.com/dwarvesf/icy-backend/internal/utils/logger" + "gorm.io/gorm" ) type Telemetry struct { + db *gorm.DB + store *store.Store appConfig *config.AppConfig logger *logger.Logger + btcRpc btcrpc.IBtcRpc } -func New(appConfig *config.AppConfig, logger *logger.Logger) *Telemetry { +func New(db *gorm.DB, store *store.Store, appConfig *config.AppConfig, logger *logger.Logger, btcRpc btcrpc.IBtcRpc) *Telemetry { return &Telemetry{ + db: db, + store: store, appConfig: appConfig, logger: logger, + btcRpc: btcRpc, + } +} + +func (t *Telemetry) IndexBtcTransaction() error { + t.logger.Info("[IndexBtcTransaction] Start indexing BTC transactions...") + + var latestTx *model.OnchainBtcTransaction + latestTx, err := t.store.OnchainBtcTransaction.GetLatestTransaction(t.db) + if err != nil { + if !errors.Is(err, gorm.ErrRecordNotFound) { + t.logger.Error("[IndexBtcTransaction][GetLatestTransaction]", map[string]string{ + "error": err.Error(), + }) + return err + } } + + txHash := "" + if latestTx != nil { + txHash = latestTx.TransactionHash + } + + t.logger.Info(fmt.Sprintf("[IndexBtcTransaction] Latest BTC transaction: %s", txHash)) + + txs, err := t.btcRpc.GetTransactionsByAddress(t.appConfig.Blockchain.BTCTreasuryAddress, txHash) + if err != nil { + t.logger.Error("[IndexBtcTransaction][GetTransactionsByAddress]", map[string]string{ + "error": err.Error(), + }) + return err + } + + return store.DoInTx(t.db, func(tx *gorm.DB) error { + for _, onchainTx := range txs { + _, err := t.store.OnchainBtcTransaction.Create(tx, &onchainTx) + if err != nil { + t.logger.Error("[IndexBtcTransaction][Create]", map[string]string{ + "error": err.Error(), + }) + return err + } + t.logger.Info(fmt.Sprintf("Tx Hash: %s - Amount: %s [%s]", onchainTx.TransactionHash, onchainTx.Amount, onchainTx.Type)) + } + return nil + }) } diff --git a/migrations/schema/0002_add_onchain_transaction_tables.up.sql b/migrations/schema/0002_add_onchain_transaction_tables.up.sql index 58c676b..edcc131 100644 --- a/migrations/schema/0002_add_onchain_transaction_tables.up.sql +++ b/migrations/schema/0002_add_onchain_transaction_tables.up.sql @@ -3,11 +3,11 @@ CREATE TABLE "onchain_icy_transactions" ( "id" SERIAL PRIMARY KEY, "internal_id" VARCHAR NOT NULL, "transaction_hash" VARCHAR NOT NULL, - "transaction_timestamp" TIMESTAMP NOT NULL, + "block_time" INTEGER, "type" VARCHAR NOT NULL, "amount" VARCHAR NOT NULL, - "sender_address" VARCHAR NOT NULL, - "receiver_address" VARCHAR NOT NULL, + "other_address" VARCHAR NOT NULL, + "fee" VARCHAR, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now() ); @@ -16,11 +16,11 @@ CREATE TABLE "onchain_btc_transactions" ( "id" SERIAL PRIMARY KEY, "internal_id" VARCHAR NOT NULL, "transaction_hash" VARCHAR NOT NULL, - "transaction_timestamp" TIMESTAMP NOT NULL, + "block_time" INTEGER, "type" VARCHAR NOT NULL, "amount" VARCHAR NOT NULL, - "sender_address" VARCHAR NOT NULL, - "receiver_address" VARCHAR NOT NULL, + "other_address" VARCHAR NOT NULL, + "fee" VARCHAR, "created_at" TIMESTAMP NOT NULL DEFAULT now(), "updated_at" TIMESTAMP NOT NULL DEFAULT now() ); From 9b6ebb226bf883992d4c94c7d24b64d674c57e5b Mon Sep 17 00:00:00 2001 From: nnhuyhoang Date: Wed, 20 Nov 2024 18:26:13 +0700 Subject: [PATCH 3/3] feat: add telemetry package --- internal/btcrpc/btcrpc.go | 3 --- internal/telemetry/telemetry.go | 31 +++++++++++++++++++++++++------ 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/internal/btcrpc/btcrpc.go b/internal/btcrpc/btcrpc.go index 6702c0e..93e6752 100644 --- a/internal/btcrpc/btcrpc.go +++ b/internal/btcrpc/btcrpc.go @@ -2,7 +2,6 @@ package btcrpc import ( "fmt" - "slices" "strconv" "github.com/btcsuite/btcd/btcutil" @@ -131,8 +130,6 @@ func (b *BtcRpc) GetTransactionsByAddress(address string, fromTxId string) ([]mo } } - slices.Reverse(confirmedTx) - transactions := make([]model.OnchainBtcTransaction, 0) for _, tx := range confirmedTx { var isOutgoing bool diff --git a/internal/telemetry/telemetry.go b/internal/telemetry/telemetry.go index f9d8533..09e6bfa 100644 --- a/internal/telemetry/telemetry.go +++ b/internal/telemetry/telemetry.go @@ -3,6 +3,7 @@ package telemetry import ( "errors" "fmt" + "slices" "github.com/dwarvesf/icy-backend/internal/btcrpc" "github.com/dwarvesf/icy-backend/internal/model" @@ -44,6 +45,7 @@ func (t *Telemetry) IndexBtcTransaction() error { } } + //TODO: Should add first transaction to db manually. txHash := "" if latestTx != nil { txHash = latestTx.TransactionHash @@ -51,14 +53,31 @@ func (t *Telemetry) IndexBtcTransaction() error { t.logger.Info(fmt.Sprintf("[IndexBtcTransaction] Latest BTC transaction: %s", txHash)) - txs, err := t.btcRpc.GetTransactionsByAddress(t.appConfig.Blockchain.BTCTreasuryAddress, txHash) - if err != nil { - t.logger.Error("[IndexBtcTransaction][GetTransactionsByAddress]", map[string]string{ - "error": err.Error(), - }) - return err + markedTxHash := "" + txs := []model.OnchainBtcTransaction{} + for { + markedTxs, err := t.btcRpc.GetTransactionsByAddress(t.appConfig.Blockchain.BTCTreasuryAddress, markedTxHash) + if err != nil { + t.logger.Error("[IndexBtcTransaction][GetTransactionsByAddress]", map[string]string{ + "error": err.Error(), + }) + return err + } + for i, tx := range markedTxs { + if tx.TransactionHash == txHash { + markedTxs = markedTxs[:i] + break + } + } + txs = append(txs, markedTxs...) + if len(markedTxs) < 25 { + break + } + markedTxHash = markedTxs[len(markedTxs)-1].TransactionHash } + slices.Reverse(txs) + return store.DoInTx(t.db, func(tx *gorm.DB) error { for _, onchainTx := range txs { _, err := t.store.OnchainBtcTransaction.Create(tx, &onchainTx)