From 7b409f581d3833f522cafaeb400aa11f0cfb58b5 Mon Sep 17 00:00:00 2001 From: sekulicd Date: Sat, 14 Sep 2024 11:14:51 +0200 Subject: [PATCH 01/15] empty config check & version flag support --- client/main.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/client/main.go b/client/main.go index a70e08ae7..741b1dd47 100644 --- a/client/main.go +++ b/client/main.go @@ -23,13 +23,13 @@ const ( ) var ( - version = "alpha" + Version string arkSdkClient arksdk.ArkClient ) func main() { app := cli.NewApp() - app.Version = version + app.Version = Version app.Name = "Ark CLI" app.Usage = "ark wallet command line interface" app.Commands = append( @@ -216,12 +216,18 @@ func config(ctx *cli.Context) error { if err != nil { return err } + cfgData, err := cfgStore.GetData(ctx.Context) if err != nil { return err } - cfg := map[string]interface{}{ + cfg := map[string]interface{}{} + if cfgData == nil { + return printJSON("no configuration found, run 'init' command") + } + + cfg = map[string]interface{}{ "asp_url": cfgData.AspUrl, "asp_pubkey": hex.EncodeToString(cfgData.AspPubkey.SerializeCompressed()), "wallet_type": cfgData.WalletType, From f787e1961b75d02c0120b40086f19eb2234489b2 Mon Sep 17 00:00:00 2001 From: Dusan Sekulic Date: Sat, 14 Sep 2024 11:24:33 +0200 Subject: [PATCH 02/15] fix: empty config check & version flag support (#309) --- client/main.go | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/client/main.go b/client/main.go index a70e08ae7..741b1dd47 100644 --- a/client/main.go +++ b/client/main.go @@ -23,13 +23,13 @@ const ( ) var ( - version = "alpha" + Version string arkSdkClient arksdk.ArkClient ) func main() { app := cli.NewApp() - app.Version = version + app.Version = Version app.Name = "Ark CLI" app.Usage = "ark wallet command line interface" app.Commands = append( @@ -216,12 +216,18 @@ func config(ctx *cli.Context) error { if err != nil { return err } + cfgData, err := cfgStore.GetData(ctx.Context) if err != nil { return err } - cfg := map[string]interface{}{ + cfg := map[string]interface{}{} + if cfgData == nil { + return printJSON("no configuration found, run 'init' command") + } + + cfg = map[string]interface{}{ "asp_url": cfgData.AspUrl, "asp_pubkey": hex.EncodeToString(cfgData.AspPubkey.SerializeCompressed()), "wallet_type": cfgData.WalletType, From 53618f0b1c0c241c98fdfe857a7aa6ae6508da85 Mon Sep 17 00:00:00 2001 From: sekulicd Date: Sat, 14 Sep 2024 11:28:04 +0200 Subject: [PATCH 03/15] fix --- client/main.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/main.go b/client/main.go index 741b1dd47..6e80a399b 100644 --- a/client/main.go +++ b/client/main.go @@ -224,7 +224,8 @@ func config(ctx *cli.Context) error { cfg := map[string]interface{}{} if cfgData == nil { - return printJSON("no configuration found, run 'init' command") + fmt.Println("no configuration found, run 'init' command") + return nil } cfg = map[string]interface{}{ From 5acf6edd1a5ca5b0e8503f96827d401624ad4aa0 Mon Sep 17 00:00:00 2001 From: sekulicd Date: Tue, 17 Sep 2024 17:07:09 +0200 Subject: [PATCH 04/15] test vtxosToTxsCovenant --- pkg/client-sdk/client_test.go | 113 +++++++++++++++++++++++++++++++++- 1 file changed, 112 insertions(+), 1 deletion(-) diff --git a/pkg/client-sdk/client_test.go b/pkg/client-sdk/client_test.go index bf6158a73..216ebd085 100644 --- a/pkg/client-sdk/client_test.go +++ b/pkg/client-sdk/client_test.go @@ -11,7 +11,57 @@ import ( "github.com/stretchr/testify/require" ) -func TestVtxosToTxs(t *testing.T) { +func TestVtxosToTxsCovenant(t *testing.T) { + tests := []struct { + name string + fixture string + want []Transaction + }{ + { + name: "Alice Sends to Bob", + fixture: aliceToBobCovenant, + want: []Transaction{ + { + Amount: 100000000, + Type: TxReceived, + Pending: false, + Claimed: true, + }, + { + Amount: 20000, + Type: TxSent, + Pending: false, + Claimed: true, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + vtxos, boardingTxs, err := loadFixtures(tt.fixture) + if err != nil { + t.Fatalf("failed to load fixture: %s", err) + } + got, err := vtxosToTxsCovenant(30, vtxos.spendable, vtxos.spent, boardingTxs) + require.NoError(t, err) + require.Len(t, got, len(tt.want)) + + // Check each expected transaction, excluding CreatedAt + for i, wantTx := range tt.want { + gotTx := got[i] + require.Equal(t, wantTx.RoundTxid, gotTx.RoundTxid) + require.Equal(t, wantTx.RedeemTxid, gotTx.RedeemTxid) + require.Equal(t, int(wantTx.Amount), int(gotTx.Amount)) + require.Equal(t, wantTx.Type, gotTx.Type) + require.Equal(t, wantTx.Pending, gotTx.Pending) + require.Equal(t, wantTx.Claimed, gotTx.Claimed) + } + }) + } +} + +func TestVtxosToTxsCovenantless(t *testing.T) { tests := []struct { name string fixture string @@ -308,6 +358,67 @@ func parseTimestamp(timestamp string) (time.Time, error) { return time.Unix(seconds, 0), nil } +var ( + // bellow fixtures are used in bellow scenario: + // 1. Alice onboards with 100000000 + // 2. Alice sends 1000 to Bob + aliceToBobCovenant = ` + { + "boardingTxs": [ + { + "boardingTxid": "69ccb6520e0b91ac1cbaa459b16ec1e3ff5f6349990b0d149dd8e6c6485d316c", + "roundTxid": "377fa2fbd27c82bdbc095478384c88b6c75432c0ef464189e49c965194446cdf", + "amount": 20000, + "pending": false, + "claimed": true, + "createdAt": "1726503865" + } + ], + "spendableVtxos": [ + { + "outpoint": { + "vtxoInput": { + "txid": "979da550421b4c06e584eedf9f6d63fb2d63b569c2e65fe6aa83fa6c3b52df7a", + "vout": 0 + } + }, + "receiver": { + "address": "tark1qvv3y4ggp43h7rre628w88zrt6l4f3du8d5dgkalqtzykqa0jwyt7qujxr20fwy8spgdfnfq4cwhs2y8djuwpr4fc4mnwjpqkc4v3kwmzsqk3u3r", + "amount": "99980000" + }, + "spent": false, + "poolTxid": "31e744a81cdd7fcc5517130a7f35722bea4dbf73faa4f4c580a6b93b2df0746d", + "spentBy": "", + "expireAt": "1726582783", + "swept": false, + "pending": false, + "pendingData": null + } + ], + "spentVtxos": [ + { + "outpoint": { + "vtxoInput": { + "txid": "213c5b329e022cacf8b38702b1d51479dfb00c204f696e43ccda0335be92c19a", + "vout": 0 + } + }, + "receiver": { + "address": "tark1qvv3y4ggp43h7rre628w88zrt6l4f3du8d5dgkalqtzykqa0jwyt7qujxr20fwy8spgdfnfq4cwhs2y8djuwpr4fc4mnwjpqkc4v3kwmzsqk3u3r", + "amount": "100000000" + }, + "spent": true, + "poolTxid": "52dd02e90d70e2ca24f3e0d41bf6382ae98efaa99177036dc261df93a5790d7d", + "spentBy": "31e744a81cdd7fcc5517130a7f35722bea4dbf73faa4f4c580a6b93b2df0746d", + "expireAt": "1726582574", + "swept": false, + "pending": false, + "pendingData": null + } + ] + }` +) + // bellow fixtures are used in bellow scenario: // 1. Alice boards with 20OOO // 2. Alice sends 1000 to Bob From ab21a3353e337acb1b3f0f5fd73ac805004a303b Mon Sep 17 00:00:00 2001 From: sekulicd Date: Thu, 19 Sep 2024 16:00:52 +0200 Subject: [PATCH 05/15] Implement background VTXO syncing and database insertion - Add listenForVtxos function to poll server for new VTXOs - Implement listenToVtxoChan to process and insert VTXOs and transactions - Handle both spendable and spent VTXOs, as well as boarding transactions - Use goroutines and channels for asynchronous processing - Add error handling and logging for better debugging --- client/main.go | 28 +- pkg/client-sdk/Makefile | 30 ++ pkg/client-sdk/ark_sdk.go | 3 +- pkg/client-sdk/client.go | 71 ++++- pkg/client-sdk/client_test.go | 63 +++-- pkg/client-sdk/covenant_client.go | 264 ++++++++++++++++-- pkg/client-sdk/covenantless_client.go | 264 ++++++++++++++++-- .../example/covenant/alice_to_bob.go | 29 +- .../example/covenantless/alice_to_bob.go | 7 +- pkg/client-sdk/store/domain.go | 54 ++++ pkg/client-sdk/store/sqlite/app_data_store.go | 96 +++++++ ...40918074848_add_transaction_table.down.sql | 3 + ...0240918074848_add_transaction_table.up.sql | 25 ++ pkg/client-sdk/store/sqlite/sqlc.yaml | 12 + .../store/sqlite/sqlc/queries/db.go | 31 ++ .../store/sqlite/sqlc/queries/models.go | 34 +++ .../store/sqlite/sqlc/queries/query.sql.go | 125 +++++++++ pkg/client-sdk/store/sqlite/sqlc/query.sql | 12 + .../store/sqlite/transaction_repository.go | 141 ++++++++++ .../store/sqlite/vtxo_repository.go | 147 ++++++++++ pkg/client-sdk/store/store.go | 37 +-- pkg/client-sdk/types.go | 19 -- server/Makefile | 2 +- .../db/sqlite/sqlc/queries/db.go | 2 +- .../db/sqlite/sqlc/queries/models.go | 2 +- .../db/sqlite/sqlc/queries/query.sql.go | 2 +- 26 files changed, 1357 insertions(+), 146 deletions(-) create mode 100644 pkg/client-sdk/store/domain.go create mode 100644 pkg/client-sdk/store/sqlite/app_data_store.go create mode 100644 pkg/client-sdk/store/sqlite/migration/20240918074848_add_transaction_table.down.sql create mode 100644 pkg/client-sdk/store/sqlite/migration/20240918074848_add_transaction_table.up.sql create mode 100644 pkg/client-sdk/store/sqlite/sqlc.yaml create mode 100644 pkg/client-sdk/store/sqlite/sqlc/queries/db.go create mode 100644 pkg/client-sdk/store/sqlite/sqlc/queries/models.go create mode 100644 pkg/client-sdk/store/sqlite/sqlc/queries/query.sql.go create mode 100644 pkg/client-sdk/store/sqlite/sqlc/query.sql create mode 100644 pkg/client-sdk/store/sqlite/transaction_repository.go create mode 100644 pkg/client-sdk/store/sqlite/vtxo_repository.go diff --git a/client/main.go b/client/main.go index 3b22746ca..d214084fb 100644 --- a/client/main.go +++ b/client/main.go @@ -14,17 +14,21 @@ import ( arksdk "github.com/ark-network/ark/pkg/client-sdk" "github.com/ark-network/ark/pkg/client-sdk/store" filestore "github.com/ark-network/ark/pkg/client-sdk/store/file" + sqlitestore "github.com/ark-network/ark/pkg/client-sdk/store/sqlite" "github.com/urfave/cli/v2" "golang.org/x/term" ) const ( DatadirEnvVar = "ARK_WALLET_DATADIR" + sqliteDir = "sqlite" ) var ( Version string arkSdkClient arksdk.ArkClient + + appDataStoreMigrationPath = os.Getenv("ARK_APP_DATA_STORE_MIGRATION_PATH") ) func main() { @@ -48,6 +52,9 @@ func main() { networkFlag, } app.Before = func(ctx *cli.Context) error { + if appDataStoreMigrationPath == "" { + appDataStoreMigrationPath = "file://../pkg/client-sdk/store/sqlite/migrations" + } sdk, err := getArkSdkClient(ctx) if err != nil { return fmt.Errorf("error initializing ark sdk client: %v", err) @@ -382,10 +389,18 @@ func redeem(ctx *cli.Context) error { } func getArkSdkClient(ctx *cli.Context) (arksdk.ArkClient, error) { - cfgStore, err := getConfigStore(ctx.String(datadirFlag.Name)) + dataDir := ctx.String(datadirFlag.Name) + cfgStore, err := getConfigStore(dataDir) + if err != nil { + return nil, err + } + + dbDir := fmt.Sprintf("%s/%s", dataDir, sqliteDir) + appDataStore, err := sqlitestore.NewAppDataRepository(dbDir, appDataStoreMigrationPath) if err != nil { return nil, err } + cfgData, err := cfgStore.GetData(ctx.Context) if err != nil { return nil, err @@ -400,22 +415,23 @@ func getArkSdkClient(ctx *cli.Context) (arksdk.ArkClient, error) { if isBtcChain(net) { return loadOrCreateClient( - arksdk.LoadCovenantlessClient, arksdk.NewCovenantlessClient, cfgStore, + arksdk.LoadCovenantlessClient, arksdk.NewCovenantlessClient, cfgStore, appDataStore, ) } return loadOrCreateClient( - arksdk.LoadCovenantClient, arksdk.NewCovenantClient, cfgStore, + arksdk.LoadCovenantClient, arksdk.NewCovenantClient, cfgStore, appDataStore, ) } func loadOrCreateClient( - loadFunc, newFunc func(store.ConfigStore) (arksdk.ArkClient, error), + loadFunc, newFunc func(store.ConfigStore, store.AppDataStore) (arksdk.ArkClient, error), store store.ConfigStore, + appDataStore store.AppDataStore, ) (arksdk.ArkClient, error) { - client, err := loadFunc(store) + client, err := loadFunc(store, appDataStore) if err != nil { if errors.Is(err, arksdk.ErrNotInitialized) { - return newFunc(store) + return newFunc(store, appDataStore) } return nil, err } diff --git a/pkg/client-sdk/Makefile b/pkg/client-sdk/Makefile index ff4951c96..a87370347 100644 --- a/pkg/client-sdk/Makefile +++ b/pkg/client-sdk/Makefile @@ -32,3 +32,33 @@ build-wasm: @mkdir -p $(BUILD_DIR) @echo "Version: $(VERSION)" @GOOS=js GOARCH=wasm GO111MODULE=on go build -ldflags="-X 'main.Version=$(VERSION)'" -o $(BUILD_DIR)/ark-sdk.wasm $(WASM_DIR)/main.go + +### Sqlc + +## mig_file: creates pg migration file(eg. make FILE=init mig_file) +mig_file: + @migrate create -ext sql -dir ./store/sqlite/migration/ $(FILE) + +## mig_up: creates db schema for provided db path +mig_up: + @echo "creating db schema..." + @migrate -database "sqlite://$(DB_PATH)/sqlite.db" -path ./store/sqlite/migration/ up + +## mig_down: apply down migration +mig_down: + @echo "migration down..." + @migrate -database "sqlite://$(DB_PATH)/sqlite.db" -path ./store/sqlite/migration/ down + +## mig_down_yes: apply down migration without prompt +mig_down_yes: + @echo "migration down..." + @"yes" | migrate -database "sqlite://path/to/database" -path ./store/sqlite/migration/ down + +## vet_db: check if mig_up and mig_down are ok +vet_db: recreatedb mig_up mig_down_yes + @echo "vet db migration scripts..." + +## sqlc: gen sql +sqlc: + @echo "gen sql..." + cd ./store/sqlite; sqlc generate \ No newline at end of file diff --git a/pkg/client-sdk/ark_sdk.go b/pkg/client-sdk/ark_sdk.go index f44f62642..0ba353623 100644 --- a/pkg/client-sdk/ark_sdk.go +++ b/pkg/client-sdk/ark_sdk.go @@ -27,8 +27,9 @@ type ArkClient interface { SendAsync(ctx context.Context, withExpiryCoinselect bool, receivers []Receiver) (string, error) Claim(ctx context.Context) (string, error) ListVtxos(ctx context.Context) (spendable, spent []client.Vtxo, err error) - GetTransactionHistory(ctx context.Context) ([]Transaction, error) Dump(ctx context.Context) (seed string, err error) + GetTransactionHistory(ctx context.Context) ([]store.Transaction, error) + GetTransactionEventChannel() chan store.Transaction } type Receiver interface { diff --git a/pkg/client-sdk/client.go b/pkg/client-sdk/client.go index fcd36e9ac..cd2e59049 100644 --- a/pkg/client-sdk/client.go +++ b/pkg/client-sdk/client.go @@ -18,6 +18,7 @@ import ( filestore "github.com/ark-network/ark/pkg/client-sdk/wallet/singlekey/store/file" inmemorystore "github.com/ark-network/ark/pkg/client-sdk/wallet/singlekey/store/inmemory" "github.com/decred/dcrd/dcrec/secp256k1/v4" + log "github.com/sirupsen/logrus" ) const ( @@ -51,12 +52,23 @@ var ( } ) +const ( + vtxoSpent spent = true + vtxoUnspent spent = false +) + +type spent bool + type arkClient struct { *store.StoreData - wallet wallet.WalletService - store store.ConfigStore - explorer explorer.Explorer - client client.ASPClient + wallet wallet.WalletService + store store.ConfigStore + appDataStore store.AppDataStore + explorer explorer.Explorer + client client.ASPClient + + vtxosChan chan map[spent]client.Vtxo + vtxoListeningStarted bool } func (a *arkClient) GetConfigData( @@ -130,6 +142,9 @@ func (a *arkClient) InitWithWallet( a.explorer = explorerSvc a.client = clientSvc + a.vtxosChan = make(chan map[spent]client.Vtxo) + a.listenForVtxos(ctx, a.vtxosChan) + return nil } @@ -201,6 +216,9 @@ func (a *arkClient) Init( a.explorer = explorerSvc a.client = clientSvc + a.vtxosChan = make(chan map[spent]client.Vtxo) + a.listenForVtxos(ctx, a.vtxosChan) + return nil } @@ -250,6 +268,51 @@ func (a *arkClient) ListVtxos( return } +func (a *arkClient) GetTransactionHistory( + ctx context.Context, +) ([]store.Transaction, error) { + return a.appDataStore.TransactionRepository().GetAll(ctx) +} + +func (a *arkClient) listenForVtxos( + ctx context.Context, + vtxoChan chan<- map[spent]client.Vtxo, +) { + go func() { + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + spendableVtxos, spentVtxos, err := a.ListVtxos(ctx) + if err != nil { + log.Warnf("listenForNewVtxos: failed to list vtxos: %s", err) + continue + } + + allVtxos := make(map[spent]client.Vtxo) + for _, vtxo := range spendableVtxos { + allVtxos[vtxoUnspent] = vtxo + } + for _, vtxo := range spentVtxos { + allVtxos[vtxoSpent] = vtxo + } + + go func() { + if len(allVtxos) > 0 { + vtxoChan <- allVtxos + } + }() + } + } + }() + + a.vtxoListeningStarted = true +} + func (a *arkClient) ping( ctx context.Context, paymentID string, ) func() { diff --git a/pkg/client-sdk/client_test.go b/pkg/client-sdk/client_test.go index 216ebd085..0a7b94dc5 100644 --- a/pkg/client-sdk/client_test.go +++ b/pkg/client-sdk/client_test.go @@ -8,6 +8,7 @@ import ( "time" "github.com/ark-network/ark/pkg/client-sdk/client" + "github.com/ark-network/ark/pkg/client-sdk/store" "github.com/stretchr/testify/require" ) @@ -15,21 +16,21 @@ func TestVtxosToTxsCovenant(t *testing.T) { tests := []struct { name string fixture string - want []Transaction + want []store.Transaction }{ { name: "Alice Sends to Bob", fixture: aliceToBobCovenant, - want: []Transaction{ + want: []store.Transaction{ { Amount: 100000000, - Type: TxReceived, + Type: store.TxReceived, Pending: false, Claimed: true, }, { Amount: 20000, - Type: TxSent, + Type: store.TxSent, Pending: false, Claimed: true, }, @@ -65,16 +66,16 @@ func TestVtxosToTxsCovenantless(t *testing.T) { tests := []struct { name string fixture string - want []Transaction + want []store.Transaction }{ { name: "Alice Before Sending Async", fixture: aliceBeforeSendingAsync, - want: []Transaction{ + want: []store.Transaction{ { RoundTxid: "377fa2fbd27c82bdbc095478384c88b6c75432c0ef464189e49c965194446cdf", Amount: 20000, - Type: TxReceived, + Type: store.TxReceived, Pending: false, Claimed: true, CreatedAt: time.Unix(1726054898, 0), @@ -84,11 +85,11 @@ func TestVtxosToTxsCovenantless(t *testing.T) { { name: "Alice After Sending Async", fixture: aliceAfterSendingAsync, - want: []Transaction{ + want: []store.Transaction{ { RoundTxid: "377fa2fbd27c82bdbc095478384c88b6c75432c0ef464189e49c965194446cdf", Amount: 20000, - Type: TxReceived, + Type: store.TxReceived, Pending: false, Claimed: true, CreatedAt: time.Unix(1726054898, 0), @@ -96,7 +97,7 @@ func TestVtxosToTxsCovenantless(t *testing.T) { { RedeemTxid: "94fa598302f17f00c8881e742ec0ce2f8c8d16f3d54fe6ba0fb7d13a493d84ad", Amount: 1000, - Type: TxSent, + Type: store.TxSent, Pending: true, Claimed: false, CreatedAt: time.Unix(1726054898, 0), @@ -106,11 +107,11 @@ func TestVtxosToTxsCovenantless(t *testing.T) { { name: "Bob Before Claiming Async", fixture: bobBeforeClaimingAsync, - want: []Transaction{ + want: []store.Transaction{ { RedeemTxid: "766fc46ba5c2da41cd4c4bc0566e0f4e0f24c184c41acd3bead5cd7b11120367", Amount: 2000, - Type: TxReceived, + Type: store.TxReceived, Pending: true, Claimed: false, CreatedAt: time.Unix(1726486359, 0), @@ -118,7 +119,7 @@ func TestVtxosToTxsCovenantless(t *testing.T) { { RedeemTxid: "94fa598302f17f00c8881e742ec0ce2f8c8d16f3d54fe6ba0fb7d13a493d84ad", Amount: 1000, - Type: TxReceived, + Type: store.TxReceived, Pending: true, Claimed: false, CreatedAt: time.Unix(1726054898, 0), @@ -128,11 +129,11 @@ func TestVtxosToTxsCovenantless(t *testing.T) { { name: "Bob After Claiming Async", fixture: bobAfterClaimingAsync, - want: []Transaction{ + want: []store.Transaction{ { RedeemTxid: "766fc46ba5c2da41cd4c4bc0566e0f4e0f24c184c41acd3bead5cd7b11120367", Amount: 2000, - Type: TxReceived, + Type: store.TxReceived, Pending: false, Claimed: true, CreatedAt: time.Unix(1726486359, 0), @@ -140,7 +141,7 @@ func TestVtxosToTxsCovenantless(t *testing.T) { { RedeemTxid: "94fa598302f17f00c8881e742ec0ce2f8c8d16f3d54fe6ba0fb7d13a493d84ad", Amount: 1000, - Type: TxReceived, + Type: store.TxReceived, Pending: false, Claimed: true, CreatedAt: time.Unix(1726054898, 0), @@ -150,11 +151,11 @@ func TestVtxosToTxsCovenantless(t *testing.T) { { name: "Bob After Sending Async", fixture: bobAfterSendingAsync, - want: []Transaction{ + want: []store.Transaction{ { RedeemTxid: "23c3a885f0ea05f7bdf83f3bf7f8ac9dc3f791ad292f4e63a6f53fa5e4935ab0", Amount: 2100, - Type: TxSent, + Type: store.TxSent, Pending: true, Claimed: false, CreatedAt: time.Unix(1726503865, 0), @@ -162,7 +163,7 @@ func TestVtxosToTxsCovenantless(t *testing.T) { { RedeemTxid: "766fc46ba5c2da41cd4c4bc0566e0f4e0f24c184c41acd3bead5cd7b11120367", Amount: 2000, - Type: TxReceived, + Type: store.TxReceived, Pending: false, Claimed: true, CreatedAt: time.Unix(1726486359, 0), @@ -170,7 +171,7 @@ func TestVtxosToTxsCovenantless(t *testing.T) { { RedeemTxid: "94fa598302f17f00c8881e742ec0ce2f8c8d16f3d54fe6ba0fb7d13a493d84ad", Amount: 1000, - Type: TxReceived, + Type: store.TxReceived, Pending: false, Claimed: true, CreatedAt: time.Unix(1726054898, 0), @@ -208,16 +209,16 @@ type vtxos struct { spent []client.Vtxo } -func loadFixtures(jsonStr string) (vtxos, []Transaction, error) { +func loadFixtures(jsonStr string) (vtxos, []store.Transaction, error) { var data struct { BoardingTxs []struct { - BoardingTxid string `json:"boardingTxid"` - RoundTxid string `json:"roundTxid"` - Amount uint64 `json:"amount"` - Type TxType `json:"txType"` - Pending bool `json:"pending"` - Claimed bool `json:"claimed"` - CreatedAt string `json:"createdAt"` + BoardingTxid string `json:"boardingTxid"` + RoundTxid string `json:"roundTxid"` + Amount uint64 `json:"amount"` + Type store.TxType `json:"txType"` + Pending bool `json:"pending"` + Claimed bool `json:"claimed"` + CreatedAt string `json:"createdAt"` } `json:"boardingTxs"` SpendableVtxos []struct { Outpoint struct { @@ -315,17 +316,17 @@ func loadFixtures(jsonStr string) (vtxos, []Transaction, error) { } } - boardingTxs := make([]Transaction, len(data.BoardingTxs)) + boardingTxs := make([]store.Transaction, len(data.BoardingTxs)) for i, tx := range data.BoardingTxs { createdAt, err := parseTimestamp(tx.CreatedAt) if err != nil { return vtxos{}, nil, err } - boardingTxs[i] = Transaction{ + boardingTxs[i] = store.Transaction{ BoardingTxid: tx.BoardingTxid, RoundTxid: tx.RoundTxid, Amount: tx.Amount, - Type: TxReceived, + Type: store.TxReceived, Pending: tx.Pending, Claimed: tx.Claimed, CreatedAt: createdAt, diff --git a/pkg/client-sdk/covenant_client.go b/pkg/client-sdk/covenant_client.go index f792a1301..d982feaa9 100644 --- a/pkg/client-sdk/covenant_client.go +++ b/pkg/client-sdk/covenant_client.go @@ -54,7 +54,10 @@ type covenantArkClient struct { *arkClient } -func NewCovenantClient(storeSvc store.ConfigStore) (ArkClient, error) { +func NewCovenantClient( + storeSvc store.ConfigStore, + appDataStore store.AppDataStore, +) (ArkClient, error) { data, err := storeSvc.GetData(context.Background()) if err != nil { return nil, err @@ -63,10 +66,24 @@ func NewCovenantClient(storeSvc store.ConfigStore) (ArkClient, error) { return nil, ErrAlreadyInitialized } - return &covenantArkClient{&arkClient{store: storeSvc}}, nil + cvnt := &covenantArkClient{ + arkClient: &arkClient{ + store: storeSvc, + appDataStore: appDataStore, + }, + } + + if err := cvnt.listenToVtxoChan(context.Background()); err != nil { + return nil, err + } + + return cvnt, nil } -func LoadCovenantClient(storeSvc store.ConfigStore) (ArkClient, error) { +func LoadCovenantClient( + storeSvc store.ConfigStore, + appDataStore store.AppDataStore, +) (ArkClient, error) { if storeSvc == nil { return nil, fmt.Errorf("missin store service") } @@ -96,13 +113,28 @@ func LoadCovenantClient(storeSvc store.ConfigStore) (ArkClient, error) { return nil, fmt.Errorf("faile to setup wallet: %s", err) } - return &covenantArkClient{ - &arkClient{data, walletSvc, storeSvc, explorerSvc, clientSvc}, - }, nil + cvnt := &covenantArkClient{ + &arkClient{ + StoreData: data, + wallet: walletSvc, + store: storeSvc, + appDataStore: appDataStore, + explorer: explorerSvc, + client: clientSvc, + }, + } + + if err := cvnt.listenToVtxoChan(context.Background()); err != nil { + return nil, err + } + + return cvnt, nil } func LoadCovenantClientWithWallet( - storeSvc store.ConfigStore, walletSvc wallet.WalletService, + storeSvc store.ConfigStore, + appDataStore store.AppDataStore, + walletSvc wallet.WalletService, ) (ArkClient, error) { if storeSvc == nil { return nil, fmt.Errorf("missin store service") @@ -131,9 +163,22 @@ func LoadCovenantClientWithWallet( return nil, fmt.Errorf("failed to setup explorer: %s", err) } - return &covenantArkClient{ - &arkClient{data, walletSvc, storeSvc, explorerSvc, clientSvc}, - }, nil + cvnt := &covenantArkClient{ + &arkClient{ + StoreData: data, + wallet: walletSvc, + store: storeSvc, + appDataStore: appDataStore, + explorer: explorerSvc, + client: clientSvc, + }, + } + + if err := cvnt.listenToVtxoChan(context.Background()); err != nil { + return nil, err + } + + return cvnt, nil } func (a *covenantArkClient) Balance( @@ -510,20 +555,177 @@ func (a *covenantArkClient) Claim(ctx context.Context) (string, error) { return a.selfTransferAllPendingPayments(ctx, boardingUtxos, receiver, desc) } -func (a *covenantArkClient) GetTransactionHistory(ctx context.Context) ([]Transaction, error) { - spendableVtxos, spentVtxos, err := a.ListVtxos(ctx) - if err != nil { - return nil, err - } +func (a *covenantArkClient) GetTransactionEventChannel() chan store.Transaction { + return a.appDataStore.TransactionRepository().GetEventChannel() +} - config, err := a.store.GetData(ctx) - if err != nil { - return nil, err - } +func (a *covenantArkClient) listenToVtxoChan(ctnx context.Context) error { + go func(ctx context.Context) { + for { + if !a.vtxoListeningStarted { + time.Sleep(1 * time.Second) + continue + } + + select { + case <-ctx.Done(): + return + case allVtxow := <-a.vtxosChan: + if len(allVtxow) == 0 { + continue + } + + spendableVtxosOld, spentVtxosOld, err := a.appDataStore.VtxoRepository().GetAll(ctx) + if err != nil { + log.Errorf("failed to get vtxos: %s", err) + continue + } + allBoardingTxs := a.getBoardingTxs(ctx) + oldVtxos := append(spendableVtxosOld, spentVtxosOld...) + if len(oldVtxos) == 0 { + spendableVtxos, spentVtxos := make([]client.Vtxo, 0), make([]client.Vtxo, 0) + for isSpent, vtxo := range allVtxow { + if isSpent { + spentVtxos = append(spentVtxos, vtxo) + } else { + spendableVtxos = append(spendableVtxos, vtxo) + } + } + + txs, err := vtxosToTxsCovenant(a.StoreData.RoundInterval, spendableVtxos, spentVtxos, allBoardingTxs) + if err != nil { + log.Errorf("failed to convert vtxos to txs: %s", err) + continue + } + + if err := a.appDataStore.TransactionRepository().InsertTransactions(ctx, txs); err != nil { + log.Errorf("failed to insert transaction: %s", err) + continue + } - boardingTxs := a.getBoardingTxs(ctx) + vtxos := make([]store.Vtxo, 0, len(allVtxow)) + for isSpent, v := range allVtxow { + vtxos = append(vtxos, store.Vtxo{ + Txid: v.Txid, + VOut: v.VOut, + Amount: v.Amount, + RoundTxid: v.RoundTxid, + ExpiresAt: v.ExpiresAt, + RedeemTx: v.RedeemTx, + UnconditionalForfeitTxs: v.UnconditionalForfeitTxs, + Pending: v.Pending, + SpentBy: v.SpentBy, + Spent: bool(isSpent), + }) + if err := a.appDataStore.VtxoRepository().InsertVtxos(ctx, vtxos); err != nil { + log.Errorf("failed to insert vtxo: %s", err) + continue + } + } + } else { + // find differece between old and new vtxos + allVtxosMap := make(map[string]client.Vtxo, 0) + for _, vtxo := range allVtxow { + key := fmt.Sprintf("%s:%d", vtxo.Txid, vtxo.VOut) + allVtxosMap[key] = vtxo + } + + oldVtxosMap := make(map[string]store.Vtxo, 0) + for _, vtxo := range oldVtxos { + key := fmt.Sprintf("%s:%d", vtxo.Txid, vtxo.VOut) + oldVtxosMap[key] = vtxo + } - return vtxosToTxsCovenant(config.RoundLifetime, spendableVtxos, spentVtxos, boardingTxs) + // find new vtxos + newSpendableVtxos, newSpentVtxos := make([]client.Vtxo, 0), make([]client.Vtxo, 0) + for isSpent, vtxo := range allVtxow { + key := fmt.Sprintf("%s:%d", vtxo.Txid, vtxo.VOut) + if _, ok := oldVtxosMap[key]; !ok { + if isSpent { + newSpentVtxos = append(newSpentVtxos, vtxo) + } else { + newSpendableVtxos = append(newSpendableVtxos, vtxo) + } + } + } + + oldBardingTxs, err := a.appDataStore.TransactionRepository().GetBoardingTxs(ctx) + if err != nil { + log.Errorf("failed to get boarding txs: %s", err) + continue + } + + newBoardingTxs := make([]store.Transaction, 0) + if len(oldBardingTxs) > 0 { + for _, tx := range allBoardingTxs { + found := false + for _, oldTx := range oldBardingTxs { + if tx.BoardingTxid == oldTx.BoardingTxid { + found = true + break + } + } + if !found { + newBoardingTxs = append(newBoardingTxs, tx) + } + } + } + + txs, err := vtxosToTxsCovenant(a.StoreData.RoundInterval, newSpendableVtxos, newSpentVtxos, newBoardingTxs) + if err != nil { + log.Errorf("failed to convert vtxos to txs: %s", err) + continue + } + + if err := a.appDataStore.TransactionRepository().InsertTransactions(ctx, txs); err != nil { + log.Errorf("failed to insert transaction: %s", err) + continue + } + + spendableVtxosToInsert := make([]store.Vtxo, 0, len(newSpendableVtxos)) + for _, v := range newSpendableVtxos { + spendableVtxosToInsert = append(spendableVtxosToInsert, store.Vtxo{ + Txid: v.Txid, + VOut: v.VOut, + Amount: v.Amount, + RoundTxid: v.RoundTxid, + ExpiresAt: v.ExpiresAt, + RedeemTx: v.RedeemTx, + UnconditionalForfeitTxs: v.UnconditionalForfeitTxs, + Pending: v.Pending, + SpentBy: v.SpentBy, + Spent: false, + }) + } + + spentVtxosToInsert := make([]store.Vtxo, 0, len(newSpentVtxos)) + for _, v := range newSpentVtxos { + spentVtxosToInsert = append(spentVtxosToInsert, store.Vtxo{ + Txid: v.Txid, + VOut: v.VOut, + Amount: v.Amount, + RoundTxid: v.RoundTxid, + ExpiresAt: v.ExpiresAt, + RedeemTx: v.RedeemTx, + UnconditionalForfeitTxs: v.UnconditionalForfeitTxs, + Pending: v.Pending, + SpentBy: v.SpentBy, + Spent: true, + }) + } + if err := a.appDataStore.VtxoRepository().InsertVtxos( + ctx, + append(spentVtxosToInsert, spendableVtxosToInsert...), + ); err != nil { + log.Errorf("failed to insert vtxo: %s", err) + continue + } + } + } + } + }(ctnx) + + return nil } func (a *covenantArkClient) getAllBoardingUtxos(ctx context.Context) ([]explorer.Utxo, error) { @@ -1422,7 +1624,7 @@ func (a *covenantArkClient) selfTransferAllPendingPayments( return roundTxid, nil } -func (a *covenantArkClient) getBoardingTxs(ctx context.Context) (transactions []Transaction) { +func (a *covenantArkClient) getBoardingTxs(ctx context.Context) (transactions []store.Transaction) { utxos, err := a.getClaimableBoardingUtxos(ctx) if err != nil { return nil @@ -1443,10 +1645,10 @@ func (a *covenantArkClient) getBoardingTxs(ctx context.Context) (transactions [] if isPending[u.Txid] { pending = true } - transactions = append(transactions, Transaction{ + transactions = append(transactions, store.Transaction{ BoardingTxid: u.Txid, Amount: u.Amount, - Type: TxReceived, + Type: store.TxReceived, Pending: pending, Claimed: !pending, CreatedAt: u.CreatedAt, @@ -1456,10 +1658,10 @@ func (a *covenantArkClient) getBoardingTxs(ctx context.Context) (transactions [] } func vtxosToTxsCovenant( - roundLifetime int64, spendable, spent []client.Vtxo, boardingTxs []Transaction, -) ([]Transaction, error) { - transactions := make([]Transaction, 0) - unconfirmedBoardingTxs := make([]Transaction, 0) + roundLifetime int64, spendable, spent []client.Vtxo, boardingTxs []store.Transaction, +) ([]store.Transaction, error) { + transactions := make([]store.Transaction, 0) + unconfirmedBoardingTxs := make([]store.Transaction, 0) for _, tx := range boardingTxs { emptyTime := time.Time{} if tx.CreatedAt == emptyTime { @@ -1484,9 +1686,9 @@ func vtxosToTxsCovenant( } } // what kind of tx was this? send or receive? - txType := TxReceived + txType := store.TxReceived if amount < 0 { - txType = TxSent + txType = store.TxSent } // check if is a pending tx pending := false @@ -1505,7 +1707,7 @@ func vtxosToTxsCovenant( redeemTxid = txid } // add transaction - transactions = append(transactions, Transaction{ + transactions = append(transactions, store.Transaction{ RoundTxid: v.RoundTxid, RedeemTxid: redeemTxid, Amount: uint64(math.Abs(float64(amount))), diff --git a/pkg/client-sdk/covenantless_client.go b/pkg/client-sdk/covenantless_client.go index f9faa6ab7..d7b0157a4 100644 --- a/pkg/client-sdk/covenantless_client.go +++ b/pkg/client-sdk/covenantless_client.go @@ -57,7 +57,10 @@ type covenantlessArkClient struct { *arkClient } -func NewCovenantlessClient(storeSvc store.ConfigStore) (ArkClient, error) { +func NewCovenantlessClient( + storeSvc store.ConfigStore, + appDataStore store.AppDataStore, +) (ArkClient, error) { data, err := storeSvc.GetData(context.Background()) if err != nil { return nil, err @@ -66,10 +69,24 @@ func NewCovenantlessClient(storeSvc store.ConfigStore) (ArkClient, error) { return nil, ErrAlreadyInitialized } - return &covenantlessArkClient{&arkClient{store: storeSvc}}, nil + cvnt := &covenantlessArkClient{ + arkClient: &arkClient{ + store: storeSvc, + appDataStore: appDataStore, + }, + } + + if err := cvnt.listenToVtxoChan(context.Background()); err != nil { + return nil, err + } + + return cvnt, nil } -func LoadCovenantlessClient(storeSvc store.ConfigStore) (ArkClient, error) { +func LoadCovenantlessClient( + storeSvc store.ConfigStore, + appDataStore store.AppDataStore, +) (ArkClient, error) { if storeSvc == nil { return nil, fmt.Errorf("missin store service") } @@ -99,13 +116,28 @@ func LoadCovenantlessClient(storeSvc store.ConfigStore) (ArkClient, error) { return nil, fmt.Errorf("faile to setup wallet: %s", err) } - return &covenantlessArkClient{ - &arkClient{data, walletSvc, storeSvc, explorerSvc, clientSvc}, - }, nil + cvnt := &covenantlessArkClient{ + &arkClient{ + StoreData: data, + wallet: walletSvc, + store: storeSvc, + appDataStore: appDataStore, + explorer: explorerSvc, + client: clientSvc, + }, + } + + if err := cvnt.listenToVtxoChan(context.Background()); err != nil { + return nil, err + } + + return cvnt, nil } func LoadCovenantlessClientWithWallet( - storeSvc store.ConfigStore, walletSvc wallet.WalletService, + storeSvc store.ConfigStore, + walletSvc wallet.WalletService, + appDataStore store.AppDataStore, ) (ArkClient, error) { if storeSvc == nil { return nil, fmt.Errorf("missin store service") @@ -134,9 +166,22 @@ func LoadCovenantlessClientWithWallet( return nil, fmt.Errorf("failed to setup explorer: %s", err) } - return &covenantlessArkClient{ - &arkClient{data, walletSvc, storeSvc, explorerSvc, clientSvc}, - }, nil + cvnt := &covenantlessArkClient{ + &arkClient{ + StoreData: data, + wallet: walletSvc, + store: storeSvc, + appDataStore: appDataStore, + explorer: explorerSvc, + client: clientSvc, + }, + } + + if err := cvnt.listenToVtxoChan(context.Background()); err != nil { + return nil, err + } + + return cvnt, nil } func (a *covenantlessArkClient) Balance( @@ -621,20 +666,177 @@ func (a *covenantlessArkClient) Claim(ctx context.Context) (string, error) { return a.selfTransferAllPendingPayments(ctx, pendingVtxos, boardingUtxos, receiver, desc) } -func (a *covenantlessArkClient) GetTransactionHistory(ctx context.Context) ([]Transaction, error) { - spendableVtxos, spentVtxos, err := a.ListVtxos(ctx) - if err != nil { - return nil, err - } +func (a *covenantlessArkClient) GetTransactionEventChannel() chan store.Transaction { + return a.appDataStore.TransactionRepository().GetEventChannel() +} - config, err := a.store.GetData(ctx) - if err != nil { - return nil, err - } +func (a *covenantlessArkClient) listenToVtxoChan(ctnx context.Context) error { + go func(ctx context.Context) { + for { + if !a.vtxoListeningStarted { + time.Sleep(1 * time.Second) + continue + } + + select { + case <-ctx.Done(): + return + case allVtxow := <-a.vtxosChan: + if len(allVtxow) == 0 { + continue + } + + spendableVtxosOld, spentVtxosOld, err := a.appDataStore.VtxoRepository().GetAll(ctx) + if err != nil { + log.Errorf("failed to get vtxos: %s", err) + continue + } + allBoardingTxs := a.getBoardingTxs(ctx) + oldVtxos := append(spendableVtxosOld, spentVtxosOld...) + if len(oldVtxos) == 0 { + spendableVtxos, spentVtxos := make([]client.Vtxo, 0), make([]client.Vtxo, 0) + for isSpent, vtxo := range allVtxow { + if isSpent { + spentVtxos = append(spentVtxos, vtxo) + } else { + spendableVtxos = append(spendableVtxos, vtxo) + } + } + + txs, err := vtxosToTxsCovenantless(a.StoreData.RoundInterval, spendableVtxos, spentVtxos, allBoardingTxs) + if err != nil { + log.Errorf("failed to convert vtxos to txs: %s", err) + continue + } - boardingTxs := a.getBoardingTxs(ctx) + if err := a.appDataStore.TransactionRepository().InsertTransactions(ctx, txs); err != nil { + log.Errorf("failed to insert transaction: %s", err) + continue + } - return vtxosToTxsCovenantless(config.RoundLifetime, spendableVtxos, spentVtxos, boardingTxs) + vtxos := make([]store.Vtxo, 0, len(allVtxow)) + for isSpent, v := range allVtxow { + vtxos = append(vtxos, store.Vtxo{ + Txid: v.Txid, + VOut: v.VOut, + Amount: v.Amount, + RoundTxid: v.RoundTxid, + ExpiresAt: v.ExpiresAt, + RedeemTx: v.RedeemTx, + UnconditionalForfeitTxs: v.UnconditionalForfeitTxs, + Pending: v.Pending, + SpentBy: v.SpentBy, + Spent: bool(isSpent), + }) + if err := a.appDataStore.VtxoRepository().InsertVtxos(ctx, vtxos); err != nil { + log.Errorf("failed to insert vtxo: %s", err) + continue + } + } + } else { + // find differece between old and new vtxos + allVtxosMap := make(map[string]client.Vtxo, 0) + for _, vtxo := range allVtxow { + key := fmt.Sprintf("%s:%d", vtxo.Txid, vtxo.VOut) + allVtxosMap[key] = vtxo + } + + oldVtxosMap := make(map[string]store.Vtxo, 0) + for _, vtxo := range oldVtxos { + key := fmt.Sprintf("%s:%d", vtxo.Txid, vtxo.VOut) + oldVtxosMap[key] = vtxo + } + + // find new vtxos + newSpendableVtxos, newSpentVtxos := make([]client.Vtxo, 0), make([]client.Vtxo, 0) + for isSpent, vtxo := range allVtxow { + key := fmt.Sprintf("%s:%d", vtxo.Txid, vtxo.VOut) + if _, ok := oldVtxosMap[key]; !ok { + if isSpent { + newSpentVtxos = append(newSpentVtxos, vtxo) + } else { + newSpendableVtxos = append(newSpendableVtxos, vtxo) + } + } + } + + oldBardingTxs, err := a.appDataStore.TransactionRepository().GetBoardingTxs(ctx) + if err != nil { + log.Errorf("failed to get boarding txs: %s", err) + continue + } + + newBoardingTxs := make([]store.Transaction, 0) + if len(oldBardingTxs) > 0 { + for _, tx := range allBoardingTxs { + found := false + for _, oldTx := range oldBardingTxs { + if tx.BoardingTxid == oldTx.BoardingTxid { + found = true + break + } + } + if !found { + newBoardingTxs = append(newBoardingTxs, tx) + } + } + } + + txs, err := vtxosToTxsCovenantless(a.StoreData.RoundInterval, newSpendableVtxos, newSpentVtxos, newBoardingTxs) + if err != nil { + log.Errorf("failed to convert vtxos to txs: %s", err) + continue + } + + if err := a.appDataStore.TransactionRepository().InsertTransactions(ctx, txs); err != nil { + log.Errorf("failed to insert transaction: %s", err) + continue + } + + spendableVtxosToInsert := make([]store.Vtxo, 0, len(newSpendableVtxos)) + for _, v := range newSpendableVtxos { + spendableVtxosToInsert = append(spendableVtxosToInsert, store.Vtxo{ + Txid: v.Txid, + VOut: v.VOut, + Amount: v.Amount, + RoundTxid: v.RoundTxid, + ExpiresAt: v.ExpiresAt, + RedeemTx: v.RedeemTx, + UnconditionalForfeitTxs: v.UnconditionalForfeitTxs, + Pending: v.Pending, + SpentBy: v.SpentBy, + Spent: false, + }) + } + + spentVtxosToInsert := make([]store.Vtxo, 0, len(newSpentVtxos)) + for _, v := range newSpentVtxos { + spentVtxosToInsert = append(spentVtxosToInsert, store.Vtxo{ + Txid: v.Txid, + VOut: v.VOut, + Amount: v.Amount, + RoundTxid: v.RoundTxid, + ExpiresAt: v.ExpiresAt, + RedeemTx: v.RedeemTx, + UnconditionalForfeitTxs: v.UnconditionalForfeitTxs, + Pending: v.Pending, + SpentBy: v.SpentBy, + Spent: true, + }) + } + if err := a.appDataStore.VtxoRepository().InsertVtxos( + ctx, + append(spentVtxosToInsert, spendableVtxosToInsert...), + ); err != nil { + log.Errorf("failed to insert vtxo: %s", err) + continue + } + } + } + } + }(ctnx) + + return nil } func (a *covenantlessArkClient) sendOnchain( @@ -1691,7 +1893,7 @@ func (a *covenantlessArkClient) selfTransferAllPendingPayments( return roundTxid, nil } -func (a *covenantlessArkClient) getBoardingTxs(ctx context.Context) (transactions []Transaction) { +func (a *covenantlessArkClient) getBoardingTxs(ctx context.Context) (transactions []store.Transaction) { utxos, err := a.getClaimableBoardingUtxos(ctx) if err != nil { return nil @@ -1712,10 +1914,10 @@ func (a *covenantlessArkClient) getBoardingTxs(ctx context.Context) (transaction if isPending[u.Txid] { pending = true } - transactions = append(transactions, Transaction{ + transactions = append(transactions, store.Transaction{ BoardingTxid: u.Txid, Amount: u.Amount, - Type: TxReceived, + Type: store.TxReceived, Pending: pending, Claimed: !pending, CreatedAt: u.CreatedAt, @@ -1734,10 +1936,10 @@ func findVtxosBySpentBy(allVtxos []client.Vtxo, txid string) (vtxos []client.Vtx } func vtxosToTxsCovenantless( - roundLifetime int64, spendable, spent []client.Vtxo, boardingTxs []Transaction, -) ([]Transaction, error) { - transactions := make([]Transaction, 0) - unconfirmedBoardingTxs := make([]Transaction, 0) + roundLifetime int64, spendable, spent []client.Vtxo, boardingTxs []store.Transaction, +) ([]store.Transaction, error) { + transactions := make([]store.Transaction, 0) + unconfirmedBoardingTxs := make([]store.Transaction, 0) for _, tx := range boardingTxs { emptyTime := time.Time{} if tx.CreatedAt == emptyTime { @@ -1763,9 +1965,9 @@ func vtxosToTxsCovenantless( } } // what kind of tx was this? send or receive? - txType := TxReceived + txType := store.TxReceived if amount < 0 { - txType = TxSent + txType = store.TxSent } // check if is a pending tx pending := false @@ -1784,7 +1986,7 @@ func vtxosToTxsCovenantless( redeemTxid = txid } // add transaction - transactions = append(transactions, Transaction{ + transactions = append(transactions, store.Transaction{ RoundTxid: v.RoundTxid, RedeemTxid: redeemTxid, Amount: uint64(math.Abs(float64(amount))), diff --git a/pkg/client-sdk/example/covenant/alice_to_bob.go b/pkg/client-sdk/example/covenant/alice_to_bob.go index 1ef228cf1..f6b3912aa 100644 --- a/pkg/client-sdk/example/covenant/alice_to_bob.go +++ b/pkg/client-sdk/example/covenant/alice_to_bob.go @@ -5,12 +5,16 @@ import ( "fmt" "io" "os/exec" + "path/filepath" + "runtime" "strings" "sync" "time" + "github.com/ark-network/ark/common" arksdk "github.com/ark-network/ark/pkg/client-sdk" inmemorystore "github.com/ark-network/ark/pkg/client-sdk/store/inmemory" + sqlitestore "github.com/ark-network/ark/pkg/client-sdk/store/sqlite" log "github.com/sirupsen/logrus" ) @@ -31,6 +35,8 @@ func main() { log.Fatal(err) } + logTxEvents("alice", aliceArkClient) + if err := aliceArkClient.Unlock(ctx, password); err != nil { log.Fatal(err) } @@ -74,6 +80,7 @@ func main() { if err != nil { log.Fatal(err) } + logTxEvents("bob", bobArkClient) if err := bobArkClient.Unlock(ctx, password); err != nil { log.Fatal(err) @@ -138,7 +145,17 @@ func setupArkClient() (arksdk.ArkClient, error) { if err != nil { return nil, fmt.Errorf("failed to setup store: %s", err) } - client, err := arksdk.NewCovenantClient(storeSvc) + + dbDir := fmt.Sprintf("%s/%s", common.AppDataDir("ark-example", false), "sqlite") + _, currentFile, _, _ := runtime.Caller(0) + migrationsDir := filepath.Join(filepath.Dir(currentFile), "..", "..", "store", "sqlite", "migration") + appDataStoreMigrationPath := "file://" + migrationsDir + appDataStore, err := sqlitestore.NewAppDataRepository(dbDir, appDataStoreMigrationPath) + if err != nil { + return nil, err + } + + client, err := arksdk.NewCovenantClient(storeSvc, appDataStore) if err != nil { return nil, fmt.Errorf("failed to setup ark client: %s", err) } @@ -225,3 +242,13 @@ func generateBlock() error { time.Sleep(6 * time.Second) return nil } + +func logTxEvents(wallet string, client arksdk.ArkClient) { + txsChan := client.GetTransactionEventChannel() + go func() { + for tx := range txsChan { + log.Infof("[EVENT]%s: tx event: %s, %d", wallet, tx.Type, tx.Amount) + } + }() + log.Infof("%s tx event listener started", wallet) +} diff --git a/pkg/client-sdk/example/covenantless/alice_to_bob.go b/pkg/client-sdk/example/covenantless/alice_to_bob.go index 3755da896..862b2abe6 100644 --- a/pkg/client-sdk/example/covenantless/alice_to_bob.go +++ b/pkg/client-sdk/example/covenantless/alice_to_bob.go @@ -9,8 +9,10 @@ import ( "sync" "time" + "github.com/ark-network/ark/common" arksdk "github.com/ark-network/ark/pkg/client-sdk" inmemorystore "github.com/ark-network/ark/pkg/client-sdk/store/inmemory" + sqlitestore "github.com/ark-network/ark/pkg/client-sdk/store/sqlite" log "github.com/sirupsen/logrus" ) @@ -146,7 +148,10 @@ func setupArkClient() (arksdk.ArkClient, error) { if err != nil { return nil, fmt.Errorf("failed to setup store: %s", err) } - client, err := arksdk.NewCovenantlessClient(storeSvc) + dbDir := fmt.Sprintf("%s/%s", common.AppDataDir("ark-example", false), "sqlite") + appDataStoreMigrationPath := "file://../../pkg/client-sdk/store/sqlite/migrations" + appDataStore, err := sqlitestore.NewAppDataRepository(dbDir, appDataStoreMigrationPath) + client, err := arksdk.NewCovenantlessClient(storeSvc, appDataStore) if err != nil { return nil, fmt.Errorf("failed to setup ark client: %s", err) } diff --git a/pkg/client-sdk/store/domain.go b/pkg/client-sdk/store/domain.go new file mode 100644 index 000000000..12c2374dd --- /dev/null +++ b/pkg/client-sdk/store/domain.go @@ -0,0 +1,54 @@ +package store + +import ( + "time" + + "github.com/ark-network/ark/common" + "github.com/decred/dcrd/dcrec/secp256k1/v4" +) + +type StoreData struct { + AspUrl string + AspPubkey *secp256k1.PublicKey + WalletType string + ClientType string + Network common.Network + RoundLifetime int64 + RoundInterval int64 + UnilateralExitDelay int64 + Dust uint64 + BoardingDescriptorTemplate string + ExplorerURL string +} + +const ( + TxSent TxType = "sent" + TxReceived TxType = "received" +) + +type TxType string + +type Transaction struct { + ID string //hash of VtxoKey + BoardingTxid string + RoundTxid string + RedeemTxid string + Amount uint64 + Type TxType + Pending bool + Claimed bool + CreatedAt time.Time +} + +type Vtxo struct { + Txid string + VOut uint32 + Amount uint64 + RoundTxid string + ExpiresAt *time.Time + RedeemTx string + UnconditionalForfeitTxs []string + Pending bool + SpentBy string + Spent bool +} diff --git a/pkg/client-sdk/store/sqlite/app_data_store.go b/pkg/client-sdk/store/sqlite/app_data_store.go new file mode 100644 index 000000000..7b90c812c --- /dev/null +++ b/pkg/client-sdk/store/sqlite/app_data_store.go @@ -0,0 +1,96 @@ +package sqlitestore + +import ( + "database/sql" + "errors" + "fmt" + "os" + "path/filepath" + + "github.com/ark-network/ark/pkg/client-sdk/store" + "github.com/golang-migrate/migrate/v4" + sqlitemigrate "github.com/golang-migrate/migrate/v4/database/sqlite" + _ "github.com/golang-migrate/migrate/v4/source/file" + log "github.com/sirupsen/logrus" +) + +const ( + sqliteDbFile = "appdata.sqlite.db" + driverName = "sqlite" +) + +type appDataRepository struct { + db *sql.DB + + transactionRepo store.TransactionRepository + vtxoRepo store.VtxoRepository +} + +func NewAppDataRepository( + baseDir string, migrationPath string, +) (store.AppDataStore, error) { + dbFile := filepath.Join(baseDir, sqliteDbFile) + db, err := openDb(dbFile) + if err != nil { + return nil, fmt.Errorf("failed to open db: %s", err) + } + + driver, err := sqlitemigrate.WithInstance(db, &sqlitemigrate.Config{}) + if err != nil { + return nil, err + } + + m, err := migrate.NewWithDatabaseInstance( + migrationPath, + "ark-sdk.db", + driver, + ) + if err != nil { + return nil, fmt.Errorf("failed to create migration instance: %s", err) + } + + if err := m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) { + return nil, fmt.Errorf("failed to run migrations: %s", err) + } + + return &appDataRepository{ + db: db, + transactionRepo: NewTransactionRepository(db), + vtxoRepo: NewVtxoRepository(db), + }, nil +} + +func (a *appDataRepository) TransactionRepository() store.TransactionRepository { + return a.transactionRepo +} + +func (a *appDataRepository) VtxoRepository() store.VtxoRepository { + return a.vtxoRepo +} + +func (a *appDataRepository) Stop() { + a.transactionRepo.Stop() + + if err := a.db.Close(); err != nil { + log.Warnf("failed to close app data store: %v", err) + } +} + +func openDb(dbPath string) (*sql.DB, error) { + dir := filepath.Dir(dbPath) + if _, err := os.Stat(dir); os.IsNotExist(err) { + err = os.MkdirAll(dir, 0755) + if err != nil { + return nil, fmt.Errorf("failed to create directory: %v", err) + } + } + + db, err := sql.Open(driverName, dbPath) + if err != nil { + return nil, fmt.Errorf("failed to open db: %w", err) + } + + db.SetMaxOpenConns(1) + + return db, nil +} diff --git a/pkg/client-sdk/store/sqlite/migration/20240918074848_add_transaction_table.down.sql b/pkg/client-sdk/store/sqlite/migration/20240918074848_add_transaction_table.down.sql new file mode 100644 index 000000000..5dee431bb --- /dev/null +++ b/pkg/client-sdk/store/sqlite/migration/20240918074848_add_transaction_table.down.sql @@ -0,0 +1,3 @@ +DROP TABLE IF EXISTS txs; + +DROP TABLE IF EXISTS vtxo; diff --git a/pkg/client-sdk/store/sqlite/migration/20240918074848_add_transaction_table.up.sql b/pkg/client-sdk/store/sqlite/migration/20240918074848_add_transaction_table.up.sql new file mode 100644 index 000000000..9dadd1337 --- /dev/null +++ b/pkg/client-sdk/store/sqlite/migration/20240918074848_add_transaction_table.up.sql @@ -0,0 +1,25 @@ +CREATE TABLE IF NOT EXISTS txs ( + id TEXT PRIMARY KEY, + boarding_txid TEXT NOT NULL, + round_txid TEXT NOT NULL, + redeem_txid TEXT NOT NULL, + amount INTEGER NOT NULL, + type TEXT NOT NULL, + pending BOOLEAN NOT NULL, + claimed BOOLEAN NOT NULL, + created_at INTEGER NOT NULL +); + +CREATE TABLE vtxo ( + txid TEXT NOT NULL, + vout INTEGER NOT NULL, + amount INTEGER NOT NULL, + round_txid TEXT, + expires_at TIMESTAMP, + redeem_tx TEXT, + unconditional_forfeit_txs TEXT, + pending BOOLEAN, + spent_by TEXT, + spent BOOLEAN, + PRIMARY KEY (txid, vout) +); \ No newline at end of file diff --git a/pkg/client-sdk/store/sqlite/sqlc.yaml b/pkg/client-sdk/store/sqlite/sqlc.yaml new file mode 100644 index 000000000..9b06e60f2 --- /dev/null +++ b/pkg/client-sdk/store/sqlite/sqlc.yaml @@ -0,0 +1,12 @@ +version: "2" +sql: + - engine: "sqlite" + queries: "sqlc/query.sql" + schema: "migration" + gen: + go: + package: "queries" + out: "sqlc/queries" + overrides: + - go_type: "int64" + column: "vtxo.expires_at" diff --git a/pkg/client-sdk/store/sqlite/sqlc/queries/db.go b/pkg/client-sdk/store/sqlite/sqlc/queries/db.go new file mode 100644 index 000000000..fa7857332 --- /dev/null +++ b/pkg/client-sdk/store/sqlite/sqlc/queries/db.go @@ -0,0 +1,31 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.26.0 + +package queries + +import ( + "context" + "database/sql" +) + +type DBTX interface { + ExecContext(context.Context, string, ...interface{}) (sql.Result, error) + PrepareContext(context.Context, string) (*sql.Stmt, error) + QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) + QueryRowContext(context.Context, string, ...interface{}) *sql.Row +} + +func New(db DBTX) *Queries { + return &Queries{db: db} +} + +type Queries struct { + db DBTX +} + +func (q *Queries) WithTx(tx *sql.Tx) *Queries { + return &Queries{ + db: tx, + } +} diff --git a/pkg/client-sdk/store/sqlite/sqlc/queries/models.go b/pkg/client-sdk/store/sqlite/sqlc/queries/models.go new file mode 100644 index 000000000..0341840f1 --- /dev/null +++ b/pkg/client-sdk/store/sqlite/sqlc/queries/models.go @@ -0,0 +1,34 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.26.0 + +package queries + +import ( + "database/sql" +) + +type Tx struct { + ID string + BoardingTxid string + RoundTxid string + RedeemTxid string + Amount int64 + Type string + Pending bool + Claimed bool + CreatedAt int64 +} + +type Vtxo struct { + Txid string + Vout int64 + Amount int64 + RoundTxid sql.NullString + ExpiresAt int64 + RedeemTx sql.NullString + UnconditionalForfeitTxs sql.NullString + Pending sql.NullBool + SpentBy sql.NullString + Spent sql.NullBool +} diff --git a/pkg/client-sdk/store/sqlite/sqlc/queries/query.sql.go b/pkg/client-sdk/store/sqlite/sqlc/queries/query.sql.go new file mode 100644 index 000000000..0d2aee8de --- /dev/null +++ b/pkg/client-sdk/store/sqlite/sqlc/queries/query.sql.go @@ -0,0 +1,125 @@ +// Code generated by sqlc. DO NOT EDIT. +// versions: +// sqlc v1.26.0 +// source: query.sql + +package queries + +import ( + "context" +) + +const selectAllTransactions = `-- name: SelectAllTransactions :many +SELECT id, boarding_txid, round_txid, redeem_txid, amount, type, pending, claimed, created_at FROM txs +` + +// Transaction +func (q *Queries) SelectAllTransactions(ctx context.Context) ([]Tx, error) { + rows, err := q.db.QueryContext(ctx, selectAllTransactions) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Tx + for rows.Next() { + var i Tx + if err := rows.Scan( + &i.ID, + &i.BoardingTxid, + &i.RoundTxid, + &i.RedeemTxid, + &i.Amount, + &i.Type, + &i.Pending, + &i.Claimed, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const selectAllVtxos = `-- name: SelectAllVtxos :many + +SELECT txid, vout, amount, round_txid, expires_at, redeem_tx, unconditional_forfeit_txs, pending, spent_by, spent FROM vtxo +` + +// Vtxo +func (q *Queries) SelectAllVtxos(ctx context.Context) ([]Vtxo, error) { + rows, err := q.db.QueryContext(ctx, selectAllVtxos) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Vtxo + for rows.Next() { + var i Vtxo + if err := rows.Scan( + &i.Txid, + &i.Vout, + &i.Amount, + &i.RoundTxid, + &i.ExpiresAt, + &i.RedeemTx, + &i.UnconditionalForfeitTxs, + &i.Pending, + &i.SpentBy, + &i.Spent, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} + +const selectBoardingTransaction = `-- name: SelectBoardingTransaction :many +SELECT id, boarding_txid, round_txid, redeem_txid, amount, type, pending, claimed, created_at FROM txs WHERE boarding_txid <> '' +` + +func (q *Queries) SelectBoardingTransaction(ctx context.Context) ([]Tx, error) { + rows, err := q.db.QueryContext(ctx, selectBoardingTransaction) + if err != nil { + return nil, err + } + defer rows.Close() + var items []Tx + for rows.Next() { + var i Tx + if err := rows.Scan( + &i.ID, + &i.BoardingTxid, + &i.RoundTxid, + &i.RedeemTxid, + &i.Amount, + &i.Type, + &i.Pending, + &i.Claimed, + &i.CreatedAt, + ); err != nil { + return nil, err + } + items = append(items, i) + } + if err := rows.Close(); err != nil { + return nil, err + } + if err := rows.Err(); err != nil { + return nil, err + } + return items, nil +} diff --git a/pkg/client-sdk/store/sqlite/sqlc/query.sql b/pkg/client-sdk/store/sqlite/sqlc/query.sql new file mode 100644 index 000000000..54bb19af3 --- /dev/null +++ b/pkg/client-sdk/store/sqlite/sqlc/query.sql @@ -0,0 +1,12 @@ +/* Transaction */ +-- name: SelectAllTransactions :many +SELECT * FROM txs; + +-- name: SelectBoardingTransaction :many +SELECT * FROM txs WHERE boarding_txid <> ''; + + +/* Vtxo */ + +-- name: SelectAllVtxos :many +SELECT * FROM vtxo; \ No newline at end of file diff --git a/pkg/client-sdk/store/sqlite/transaction_repository.go b/pkg/client-sdk/store/sqlite/transaction_repository.go new file mode 100644 index 000000000..038b17c25 --- /dev/null +++ b/pkg/client-sdk/store/sqlite/transaction_repository.go @@ -0,0 +1,141 @@ +package sqlitestore + +import ( + "context" + "database/sql" + "time" + + "github.com/ark-network/ark/pkg/client-sdk/store" + "github.com/ark-network/ark/pkg/client-sdk/store/sqlite/sqlc/queries" + "github.com/google/uuid" +) + +type transactionRepository struct { + db *sql.DB + querier *queries.Queries + + eventChannel chan store.Transaction +} + +func NewTransactionRepository(db *sql.DB) store.TransactionRepository { + return &transactionRepository{ + db: db, + querier: queries.New(db), + eventChannel: make(chan store.Transaction), + } +} + +const insertTransaction = ` +INSERT INTO txs ( + id, + boarding_txid, + round_txid, + redeem_txid, + amount, + type, + pending, + claimed, + created_at +) VALUES ( + ?,?, ?, ?, ?, ?, ?, ?, ? +)` + +func (t *transactionRepository) InsertTransactions(ctx context.Context, txs []store.Transaction) error { + dbTx, err := t.db.BeginTx(ctx, nil) + if err != nil { + return err + } + defer dbTx.Rollback() + + // Prepare the statement + stmt, err := dbTx.PrepareContext(ctx, insertTransaction) + if err != nil { + return err + } + defer stmt.Close() + + for _, tx := range txs { + _, err := stmt.ExecContext(ctx, + uuid.New().String(), + tx.BoardingTxid, + tx.RoundTxid, + tx.RedeemTxid, + int64(tx.Amount), + string(tx.Type), + tx.Pending, + tx.Claimed, + tx.CreatedAt.Unix(), + ) + if err != nil { + return err + } + + go func(transaction store.Transaction) { + t.eventChannel <- transaction + }(tx) + } + + if err := dbTx.Commit(); err != nil { + return err + } + + return nil +} + +func (t *transactionRepository) GetAll( + ctx context.Context, +) ([]store.Transaction, error) { + rows, err := t.querier.SelectAllTransactions(ctx) + if err != nil { + return nil, err + } + + resp := make([]store.Transaction, 0, len(rows)) + for _, row := range rows { + resp = append(resp, store.Transaction{ + ID: row.ID, + BoardingTxid: row.BoardingTxid, + RoundTxid: row.RoundTxid, + RedeemTxid: row.RedeemTxid, + Amount: uint64(row.Amount), + Type: store.TxType(row.Type), + Pending: row.Pending, + Claimed: row.Claimed, + CreatedAt: time.Unix(row.CreatedAt, 0), + }) + } + + return resp, nil +} + +func (t *transactionRepository) GetEventChannel() chan store.Transaction { + return t.eventChannel +} + +func (t *transactionRepository) Stop() { + close(t.eventChannel) +} + +func (t *transactionRepository) GetBoardingTxs(ctx context.Context) ([]store.Transaction, error) { + rows, err := t.querier.SelectBoardingTransaction(ctx) + if err != nil { + return nil, err + } + + resp := make([]store.Transaction, 0, len(rows)) + for _, row := range rows { + resp = append(resp, store.Transaction{ + ID: row.ID, + BoardingTxid: row.BoardingTxid, + RoundTxid: row.RoundTxid, + RedeemTxid: row.RedeemTxid, + Amount: uint64(row.Amount), + Type: store.TxType(row.Type), + Pending: row.Pending, + Claimed: row.Claimed, + CreatedAt: time.Unix(row.CreatedAt, 0), + }) + } + + return resp, nil +} diff --git a/pkg/client-sdk/store/sqlite/vtxo_repository.go b/pkg/client-sdk/store/sqlite/vtxo_repository.go new file mode 100644 index 000000000..3ee5b3fec --- /dev/null +++ b/pkg/client-sdk/store/sqlite/vtxo_repository.go @@ -0,0 +1,147 @@ +package sqlitestore + +import ( + "context" + "database/sql" + "strings" + "time" + + "github.com/ark-network/ark/pkg/client-sdk/store" + "github.com/ark-network/ark/pkg/client-sdk/store/sqlite/sqlc/queries" +) + +type vtxoRepository struct { + db *sql.DB + querier *queries.Queries +} + +func NewVtxoRepository(db *sql.DB) store.VtxoRepository { + return &vtxoRepository{ + db: db, + querier: queries.New(db), + } +} + +func (v *vtxoRepository) GetAll( + ctx context.Context, +) (spendable []store.Vtxo, spent []store.Vtxo, err error) { + rows, err := v.querier.SelectAllVtxos(ctx) + if err != nil { + return nil, nil, err + } + + spendableVtxos := make([]store.Vtxo, 0) + spentVxos := make([]store.Vtxo, 0) + for _, v := range rows { + roundTxID := "" + if v.RoundTxid.Valid { + roundTxID = v.RoundTxid.String + } + + redeemTx := "" + if v.RedeemTx.Valid { + redeemTx = v.RedeemTx.String + } + + unconditionalForfeitTxs := make([]string, 0) + if v.UnconditionalForfeitTxs.Valid { + unconditionalForfeitTxs = strings.Split(v.UnconditionalForfeitTxs.String, ",") + } + + pending := false + if v.Pending.Valid { + pending = v.Pending.Bool + } + + spentBy := "" + if v.SpentBy.Valid { + spentBy = v.SpentBy.String + } + + spent := false + if v.Spent.Valid { + spent = v.Spent.Bool + } + + expiresAt := time.Unix(v.ExpiresAt, 0) + + vtxo := store.Vtxo{ + Txid: v.Txid, + VOut: uint32(v.Vout), + Amount: uint64(v.Amount), + RoundTxid: roundTxID, + ExpiresAt: &expiresAt, + RedeemTx: redeemTx, + UnconditionalForfeitTxs: unconditionalForfeitTxs, + Pending: pending, + SpentBy: spentBy, + Spent: spent, + } + if spent { + spentVxos = append(spentVxos, vtxo) + } else { + spendableVtxos = append(spendableVtxos, vtxo) + } + + } + + return spendableVtxos, spentVxos, nil +} + +const insertVtxos = ` +INSERT INTO vtxo ( + txid, vout, amount, round_txid, expires_at, redeem_tx, unconditional_forfeit_txs, pending, spent_by, spent + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` + +func (v *vtxoRepository) InsertVtxos(ctx context.Context, vtxos []store.Vtxo) error { + // Start a transaction + tx, err := v.db.BeginTx(ctx, nil) + if err != nil { + return err + } + defer tx.Rollback() + + // Prepare the statement + stmt, err := tx.PrepareContext(ctx, insertVtxos) + if err != nil { + return err + } + defer stmt.Close() + + for _, vtxo := range vtxos { + var expiresAt sql.NullInt64 + if vtxo.ExpiresAt != nil { + expiresAt = sql.NullInt64{Int64: vtxo.ExpiresAt.Unix(), Valid: true} + } + + unconditionalForfeitTxs := "" + if vtxo.UnconditionalForfeitTxs != nil { + for _, tx := range vtxo.UnconditionalForfeitTxs { + unconditionalForfeitTxs += tx + "," + } + } + + _, err := stmt.ExecContext(ctx, + vtxo.Txid, + vtxo.VOut, + vtxo.Amount, + vtxo.RoundTxid, + expiresAt, + vtxo.RedeemTx, + unconditionalForfeitTxs, + vtxo.Pending, + vtxo.SpentBy, + vtxo.Spent, + ) + if err != nil { + return err + } + } + + // Commit the transaction + if err := tx.Commit(); err != nil { + return err + } + + return nil +} diff --git a/pkg/client-sdk/store/store.go b/pkg/client-sdk/store/store.go index 963379d73..d6a7dc980 100644 --- a/pkg/client-sdk/store/store.go +++ b/pkg/client-sdk/store/store.go @@ -2,9 +2,6 @@ package store import ( "context" - - "github.com/ark-network/ark/common" - "github.com/decred/dcrd/dcrec/secp256k1/v4" ) const ( @@ -12,20 +9,6 @@ const ( FileStore = "file" ) -type StoreData struct { - AspUrl string - AspPubkey *secp256k1.PublicKey - WalletType string - ClientType string - Network common.Network - RoundLifetime int64 - RoundInterval int64 - UnilateralExitDelay int64 - Dust uint64 - BoardingDescriptorTemplate string - ExplorerURL string -} - type ConfigStore interface { GetType() string GetDatadir() string @@ -33,3 +16,23 @@ type ConfigStore interface { GetData(ctx context.Context) (*StoreData, error) CleanData(ctx context.Context) error } + +type AppDataStore interface { + TransactionRepository() TransactionRepository + VtxoRepository() VtxoRepository + + Stop() +} + +type TransactionRepository interface { + InsertTransactions(ctx context.Context, txs []Transaction) error + GetAll(ctx context.Context) ([]Transaction, error) + GetEventChannel() chan Transaction + GetBoardingTxs(ctx context.Context) ([]Transaction, error) + Stop() +} + +type VtxoRepository interface { + InsertVtxos(ctx context.Context, vtxos []Vtxo) error + GetAll(ctx context.Context) (spendable []Vtxo, spent []Vtxo, err error) +} diff --git a/pkg/client-sdk/types.go b/pkg/client-sdk/types.go index 17afb81f2..8954befab 100644 --- a/pkg/client-sdk/types.go +++ b/pkg/client-sdk/types.go @@ -2,7 +2,6 @@ package arksdk import ( "fmt" - "time" grpcclient "github.com/ark-network/ark/pkg/client-sdk/client/grpc" restclient "github.com/ark-network/ark/pkg/client-sdk/client/rest" @@ -122,21 +121,3 @@ type balanceRes struct { offchainBalanceByExpiration map[int64]uint64 err error } - -const ( - TxSent TxType = "sent" - TxReceived TxType = "received" -) - -type TxType string - -type Transaction struct { - BoardingTxid string - RoundTxid string - RedeemTxid string - Amount uint64 - Type TxType - Pending bool - Claimed bool - CreatedAt time.Time -} diff --git a/server/Makefile b/server/Makefile index 86d6d4d6c..6084d1f00 100755 --- a/server/Makefile +++ b/server/Makefile @@ -108,7 +108,7 @@ mig_down_yes: @"yes" | migrate -database "sqlite://path/to/database" -path ./internal/infrastructure/db/sqlite/migration/ down ## vet_db: check if mig_up and mig_down are ok -vet_db: recreatedb mig_up mig_down_yes +vet_db: mig_up mig_down_yes @echo "vet db migration scripts..." ## sqlc: gen sql diff --git a/server/internal/infrastructure/db/sqlite/sqlc/queries/db.go b/server/internal/infrastructure/db/sqlite/sqlc/queries/db.go index 1cbab906a..fa7857332 100644 --- a/server/internal/infrastructure/db/sqlite/sqlc/queries/db.go +++ b/server/internal/infrastructure/db/sqlite/sqlc/queries/db.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.27.0 +// sqlc v1.26.0 package queries diff --git a/server/internal/infrastructure/db/sqlite/sqlc/queries/models.go b/server/internal/infrastructure/db/sqlite/sqlc/queries/models.go index f81a5af35..b87a0819e 100644 --- a/server/internal/infrastructure/db/sqlite/sqlc/queries/models.go +++ b/server/internal/infrastructure/db/sqlite/sqlc/queries/models.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.27.0 +// sqlc v1.26.0 package queries diff --git a/server/internal/infrastructure/db/sqlite/sqlc/queries/query.sql.go b/server/internal/infrastructure/db/sqlite/sqlc/queries/query.sql.go index bf7d53e8b..944227ae6 100644 --- a/server/internal/infrastructure/db/sqlite/sqlc/queries/query.sql.go +++ b/server/internal/infrastructure/db/sqlite/sqlc/queries/query.sql.go @@ -1,6 +1,6 @@ // Code generated by sqlc. DO NOT EDIT. // versions: -// sqlc v1.27.0 +// sqlc v1.26.0 // source: query.sql package queries From 120a562215ac380f14f1219504bfca4bf583653b Mon Sep 17 00:00:00 2001 From: sekulicd Date: Fri, 20 Sep 2024 11:04:54 +0200 Subject: [PATCH 06/15] fix --- pkg/client-sdk/covenant_client.go | 39 ++++++++++++------- .../example/covenant/alice_to_bob.go | 2 +- 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/pkg/client-sdk/covenant_client.go b/pkg/client-sdk/covenant_client.go index d982feaa9..e24e84600 100644 --- a/pkg/client-sdk/covenant_client.go +++ b/pkg/client-sdk/covenant_client.go @@ -598,9 +598,11 @@ func (a *covenantArkClient) listenToVtxoChan(ctnx context.Context) error { continue } - if err := a.appDataStore.TransactionRepository().InsertTransactions(ctx, txs); err != nil { - log.Errorf("failed to insert transaction: %s", err) - continue + if len(txs) > 0 { + if err := a.appDataStore.TransactionRepository().InsertTransactions(ctx, txs); err != nil { + log.Errorf("failed to insert transaction: %s", err) + continue + } } vtxos := make([]store.Vtxo, 0, len(allVtxow)) @@ -617,9 +619,11 @@ func (a *covenantArkClient) listenToVtxoChan(ctnx context.Context) error { SpentBy: v.SpentBy, Spent: bool(isSpent), }) - if err := a.appDataStore.VtxoRepository().InsertVtxos(ctx, vtxos); err != nil { - log.Errorf("failed to insert vtxo: %s", err) - continue + if len(vtxos) > 0 { + if err := a.appDataStore.VtxoRepository().InsertVtxos(ctx, vtxos); err != nil { + log.Errorf("failed to insert vtxo: %s", err) + continue + } } } } else { @@ -677,9 +681,11 @@ func (a *covenantArkClient) listenToVtxoChan(ctnx context.Context) error { continue } - if err := a.appDataStore.TransactionRepository().InsertTransactions(ctx, txs); err != nil { - log.Errorf("failed to insert transaction: %s", err) - continue + if len(txs) > 0 { + if err := a.appDataStore.TransactionRepository().InsertTransactions(ctx, txs); err != nil { + log.Errorf("failed to insert transaction: %s", err) + continue + } } spendableVtxosToInsert := make([]store.Vtxo, 0, len(newSpendableVtxos)) @@ -713,12 +719,15 @@ func (a *covenantArkClient) listenToVtxoChan(ctnx context.Context) error { Spent: true, }) } - if err := a.appDataStore.VtxoRepository().InsertVtxos( - ctx, - append(spentVtxosToInsert, spendableVtxosToInsert...), - ); err != nil { - log.Errorf("failed to insert vtxo: %s", err) - continue + + if len(spendableVtxosToInsert) > 0 || len(spentVtxosToInsert) > 0 { + if err := a.appDataStore.VtxoRepository().InsertVtxos( + ctx, + append(spentVtxosToInsert, spendableVtxosToInsert...), + ); err != nil { + log.Errorf("failed to insert vtxo: %s", err) + continue + } } } } diff --git a/pkg/client-sdk/example/covenant/alice_to_bob.go b/pkg/client-sdk/example/covenant/alice_to_bob.go index f6b3912aa..49b28f7db 100644 --- a/pkg/client-sdk/example/covenant/alice_to_bob.go +++ b/pkg/client-sdk/example/covenant/alice_to_bob.go @@ -120,7 +120,7 @@ func main() { log.Fatal(err) } - time.Sleep(5 * time.Second) + time.Sleep(240 * time.Second) aliceBalance, err = aliceArkClient.Balance(ctx, false) if err != nil { From 0f3d29b57d0eee5277c68521f0be18ccf7be2f9f Mon Sep 17 00:00:00 2001 From: sekulicd Date: Mon, 23 Sep 2024 16:12:50 +0200 Subject: [PATCH 07/15] SDK - replace sqlite with badger, merge appData with config store --- client/main.go | 69 ++-- pkg/client-sdk/Makefile | 32 +- pkg/client-sdk/ark_sdk.go | 8 +- pkg/client-sdk/client.go | 174 +++++---- pkg/client-sdk/client_test.go | 64 ++-- pkg/client-sdk/covenant_client.go | 338 ++++++++---------- pkg/client-sdk/covenantless_client.go | 331 ++++++++--------- .../example/covenant/alice_to_bob.go | 32 +- .../example/covenantless/alice_to_bob.go | 18 +- .../store/badger/app_data_repository.go | 32 ++ .../store/badger/transaction_repository.go | 68 ++++ pkg/client-sdk/store/badger/utils.go | 47 +++ .../store/badger/vtxo_repository.go | 57 +++ pkg/client-sdk/store/{ => domain}/domain.go | 9 +- .../store/{store.go => domain/repository.go} | 30 +- .../store/file/{store.go => config.go} | 51 +-- pkg/client-sdk/store/inmemory/config.go | 55 +++ pkg/client-sdk/store/inmemory/store.go | 55 --- pkg/client-sdk/store/service.go | 91 +++++ .../store/{store_test.go => service_test.go} | 87 ++++- pkg/client-sdk/store/sqlite/app_data_store.go | 96 ----- ...40918074848_add_transaction_table.down.sql | 3 - ...0240918074848_add_transaction_table.up.sql | 25 -- pkg/client-sdk/store/sqlite/sqlc.yaml | 12 - .../store/sqlite/sqlc/queries/db.go | 31 -- .../store/sqlite/sqlc/queries/models.go | 34 -- .../store/sqlite/sqlc/queries/query.sql.go | 125 ------- pkg/client-sdk/store/sqlite/sqlc/query.sql | 12 - .../store/sqlite/transaction_repository.go | 138 ------- .../store/sqlite/vtxo_repository.go | 147 -------- .../wallet/singlekey/bitcoin_wallet.go | 8 +- .../wallet/singlekey/liquid_wallet.go | 10 +- pkg/client-sdk/wallet/singlekey/wallet.go | 10 +- pkg/client-sdk/wallet/wallet_test.go | 8 +- pkg/client-sdk/wasm/browser/config_store.go | 8 +- pkg/client-sdk/wasm/browser/exports.go | 8 +- 36 files changed, 990 insertions(+), 1333 deletions(-) create mode 100644 pkg/client-sdk/store/badger/app_data_repository.go create mode 100644 pkg/client-sdk/store/badger/transaction_repository.go create mode 100644 pkg/client-sdk/store/badger/utils.go create mode 100644 pkg/client-sdk/store/badger/vtxo_repository.go rename pkg/client-sdk/store/{ => domain}/domain.go (90%) rename pkg/client-sdk/store/{store.go => domain/repository.go} (65%) rename pkg/client-sdk/store/file/{store.go => config.go} (94%) create mode 100644 pkg/client-sdk/store/inmemory/config.go delete mode 100644 pkg/client-sdk/store/inmemory/store.go create mode 100644 pkg/client-sdk/store/service.go rename pkg/client-sdk/store/{store_test.go => service_test.go} (53%) delete mode 100644 pkg/client-sdk/store/sqlite/app_data_store.go delete mode 100644 pkg/client-sdk/store/sqlite/migration/20240918074848_add_transaction_table.down.sql delete mode 100644 pkg/client-sdk/store/sqlite/migration/20240918074848_add_transaction_table.up.sql delete mode 100644 pkg/client-sdk/store/sqlite/sqlc.yaml delete mode 100644 pkg/client-sdk/store/sqlite/sqlc/queries/db.go delete mode 100644 pkg/client-sdk/store/sqlite/sqlc/queries/models.go delete mode 100644 pkg/client-sdk/store/sqlite/sqlc/queries/query.sql.go delete mode 100644 pkg/client-sdk/store/sqlite/sqlc/query.sql delete mode 100644 pkg/client-sdk/store/sqlite/transaction_repository.go delete mode 100644 pkg/client-sdk/store/sqlite/vtxo_repository.go diff --git a/client/main.go b/client/main.go index d214084fb..52cc35ce0 100644 --- a/client/main.go +++ b/client/main.go @@ -7,14 +7,12 @@ import ( "errors" "fmt" "os" - "strings" "syscall" "github.com/ark-network/ark/common" arksdk "github.com/ark-network/ark/pkg/client-sdk" "github.com/ark-network/ark/pkg/client-sdk/store" - filestore "github.com/ark-network/ark/pkg/client-sdk/store/file" - sqlitestore "github.com/ark-network/ark/pkg/client-sdk/store/sqlite" + "github.com/ark-network/ark/pkg/client-sdk/store/domain" "github.com/urfave/cli/v2" "golang.org/x/term" ) @@ -27,8 +25,6 @@ const ( var ( Version string arkSdkClient arksdk.ArkClient - - appDataStoreMigrationPath = os.Getenv("ARK_APP_DATA_STORE_MIGRATION_PATH") ) func main() { @@ -52,9 +48,6 @@ func main() { networkFlag, } app.Before = func(ctx *cli.Context) error { - if appDataStoreMigrationPath == "" { - appDataStoreMigrationPath = "file://../pkg/client-sdk/store/sqlite/migrations" - } sdk, err := getArkSdkClient(ctx) if err != nil { return fmt.Errorf("error initializing ark sdk client: %v", err) @@ -219,12 +212,12 @@ func initArkSdk(ctx *cli.Context) error { } func config(ctx *cli.Context) error { - cfgStore, err := getConfigStore(ctx.String(datadirFlag.Name)) + sdkRepository, err := getSdkRepository(ctx.String(datadirFlag.Name)) if err != nil { return err } - cfgData, err := cfgStore.GetData(ctx.Context) + cfgData, err := sdkRepository.ConfigRepository().GetData(ctx.Context) if err != nil { return err } @@ -307,16 +300,16 @@ func send(ctx *cli.Context) error { return fmt.Errorf("missing destination, use --to and --amount or --receivers") } - configStore, err := getConfigStore(ctx.String(datadirFlag.Name)) + sdkRepository, err := getSdkRepository(ctx.String(datadirFlag.Name)) if err != nil { return err } - cfgData, err := configStore.GetData(ctx.Context) + net, err := getNetwork(ctx, sdkRepository) if err != nil { return err } - net := getNetwork(ctx, cfgData) + isBitcoin := isBtcChain(net) var receivers []arksdk.Receiver @@ -390,63 +383,69 @@ func redeem(ctx *cli.Context) error { func getArkSdkClient(ctx *cli.Context) (arksdk.ArkClient, error) { dataDir := ctx.String(datadirFlag.Name) - cfgStore, err := getConfigStore(dataDir) - if err != nil { - return nil, err - } - - dbDir := fmt.Sprintf("%s/%s", dataDir, sqliteDir) - appDataStore, err := sqlitestore.NewAppDataRepository(dbDir, appDataStoreMigrationPath) + sdkRepository, err := getSdkRepository(dataDir) if err != nil { return nil, err } - cfgData, err := cfgStore.GetData(ctx.Context) + configData, err := sdkRepository.ConfigRepository().GetData(context.Background()) if err != nil { return nil, err } commandName := ctx.Args().First() - if commandName != "init" && cfgData == nil { + if commandName != "init" && configData == nil { return nil, fmt.Errorf("CLI not initialized, run 'init' cmd to initialize") } - net := getNetwork(ctx, cfgData) + net, err := getNetwork(ctx, sdkRepository) + if err != nil { + return nil, err + } if isBtcChain(net) { return loadOrCreateClient( - arksdk.LoadCovenantlessClient, arksdk.NewCovenantlessClient, cfgStore, appDataStore, + arksdk.LoadCovenantlessClient, arksdk.NewCovenantlessClient, sdkRepository, ) } return loadOrCreateClient( - arksdk.LoadCovenantClient, arksdk.NewCovenantClient, cfgStore, appDataStore, + arksdk.LoadCovenantClient, arksdk.NewCovenantClient, sdkRepository, ) } func loadOrCreateClient( - loadFunc, newFunc func(store.ConfigStore, store.AppDataStore) (arksdk.ArkClient, error), - store store.ConfigStore, - appDataStore store.AppDataStore, + loadFunc, newFunc func(domain.SdkRepository) (arksdk.ArkClient, error), + sdkRepository domain.SdkRepository, ) (arksdk.ArkClient, error) { - client, err := loadFunc(store, appDataStore) + client, err := loadFunc(sdkRepository) if err != nil { if errors.Is(err, arksdk.ErrNotInitialized) { - return newFunc(store, appDataStore) + return newFunc(sdkRepository) } return nil, err } return client, err } -func getConfigStore(dataDir string) (store.ConfigStore, error) { - return filestore.NewConfigStore(dataDir) +func getSdkRepository(dataDir string) (domain.SdkRepository, error) { + return store.NewService(store.Config{ + ConfigStoreType: store.FileStore, + AppDataStoreType: store.Badger, + BaseDir: dataDir, + }) } -func getNetwork(ctx *cli.Context, configData *store.StoreData) string { +func getNetwork(ctx *cli.Context, sdkRepository domain.SdkRepository) (string, error) { + configData, err := sdkRepository.ConfigRepository().GetData(context.Background()) + if err != nil { + return "", err + } + if configData == nil { - return strings.ToLower(ctx.String(networkFlag.Name)) + return ctx.String(networkFlag.Name), nil } - return configData.Network.Name + + return configData.Network.Name, nil } func isBtcChain(network string) bool { diff --git a/pkg/client-sdk/Makefile b/pkg/client-sdk/Makefile index a87370347..e609fc460 100644 --- a/pkg/client-sdk/Makefile +++ b/pkg/client-sdk/Makefile @@ -31,34 +31,4 @@ GO_VERSION := $(shell go version | cut -d' ' -f3) build-wasm: @mkdir -p $(BUILD_DIR) @echo "Version: $(VERSION)" - @GOOS=js GOARCH=wasm GO111MODULE=on go build -ldflags="-X 'main.Version=$(VERSION)'" -o $(BUILD_DIR)/ark-sdk.wasm $(WASM_DIR)/main.go - -### Sqlc - -## mig_file: creates pg migration file(eg. make FILE=init mig_file) -mig_file: - @migrate create -ext sql -dir ./store/sqlite/migration/ $(FILE) - -## mig_up: creates db schema for provided db path -mig_up: - @echo "creating db schema..." - @migrate -database "sqlite://$(DB_PATH)/sqlite.db" -path ./store/sqlite/migration/ up - -## mig_down: apply down migration -mig_down: - @echo "migration down..." - @migrate -database "sqlite://$(DB_PATH)/sqlite.db" -path ./store/sqlite/migration/ down - -## mig_down_yes: apply down migration without prompt -mig_down_yes: - @echo "migration down..." - @"yes" | migrate -database "sqlite://path/to/database" -path ./store/sqlite/migration/ down - -## vet_db: check if mig_up and mig_down are ok -vet_db: recreatedb mig_up mig_down_yes - @echo "vet db migration scripts..." - -## sqlc: gen sql -sqlc: - @echo "gen sql..." - cd ./store/sqlite; sqlc generate \ No newline at end of file + @GOOS=js GOARCH=wasm GO111MODULE=on go build -ldflags="-X 'main.Version=$(VERSION)'" -o $(BUILD_DIR)/ark-sdk.wasm $(WASM_DIR)/main.go \ No newline at end of file diff --git a/pkg/client-sdk/ark_sdk.go b/pkg/client-sdk/ark_sdk.go index 0ba353623..07cf1d69b 100644 --- a/pkg/client-sdk/ark_sdk.go +++ b/pkg/client-sdk/ark_sdk.go @@ -4,11 +4,11 @@ import ( "context" "github.com/ark-network/ark/pkg/client-sdk/client" - "github.com/ark-network/ark/pkg/client-sdk/store" + "github.com/ark-network/ark/pkg/client-sdk/store/domain" ) type ArkClient interface { - GetConfigData(ctx context.Context) (*store.StoreData, error) + GetConfigData(ctx context.Context) (*domain.ConfigData, error) Init(ctx context.Context, args InitArgs) error InitWithWallet(ctx context.Context, args InitWithWalletArgs) error IsLocked(ctx context.Context) bool @@ -28,8 +28,8 @@ type ArkClient interface { Claim(ctx context.Context) (string, error) ListVtxos(ctx context.Context) (spendable, spent []client.Vtxo, err error) Dump(ctx context.Context) (seed string, err error) - GetTransactionHistory(ctx context.Context) ([]store.Transaction, error) - GetTransactionEventChannel() chan store.Transaction + GetTransactionHistory(ctx context.Context) ([]domain.Transaction, error) + GetTransactionEventChannel() chan domain.Transaction } type Receiver interface { diff --git a/pkg/client-sdk/client.go b/pkg/client-sdk/client.go index 2b4ff3aee..7e51a1344 100644 --- a/pkg/client-sdk/client.go +++ b/pkg/client-sdk/client.go @@ -12,13 +12,14 @@ import ( "github.com/ark-network/ark/pkg/client-sdk/explorer" "github.com/ark-network/ark/pkg/client-sdk/internal/utils" "github.com/ark-network/ark/pkg/client-sdk/store" + "github.com/ark-network/ark/pkg/client-sdk/store/domain" "github.com/ark-network/ark/pkg/client-sdk/wallet" singlekeywallet "github.com/ark-network/ark/pkg/client-sdk/wallet/singlekey" walletstore "github.com/ark-network/ark/pkg/client-sdk/wallet/singlekey/store" filestore "github.com/ark-network/ark/pkg/client-sdk/wallet/singlekey/store/file" inmemorystore "github.com/ark-network/ark/pkg/client-sdk/wallet/singlekey/store/inmemory" "github.com/decred/dcrd/dcrec/secp256k1/v4" - "github.com/sirupsen/logrus" + log "github.com/sirupsen/logrus" ) const ( @@ -60,24 +61,22 @@ const ( type spent bool type arkClient struct { - *store.StoreData - wallet wallet.WalletService - store store.ConfigStore - appDataStore store.AppDataStore - explorer explorer.Explorer - client client.ASPClient - - vtxosChan chan map[spent]client.Vtxo - vtxoListeningStarted bool + *domain.ConfigData + sdkRepository domain.SdkRepository + wallet wallet.WalletService + explorer explorer.Explorer + client client.ASPClient + + sdkInitialized bool } func (a *arkClient) GetConfigData( _ context.Context, -) (*store.StoreData, error) { - if a.StoreData == nil { +) (*domain.ConfigData, error) { + if a.ConfigData == nil { return nil, fmt.Errorf("client sdk not initialized") } - return a.StoreData, nil + return a.ConfigData, nil } func (a *arkClient) InitWithWallet( @@ -115,7 +114,7 @@ func (a *arkClient) InitWithWallet( return fmt.Errorf("failed to parse asp pubkey: %s", err) } - storeData := store.StoreData{ + storeData := domain.ConfigData{ AspUrl: args.AspUrl, AspPubkey: aspPubkey, WalletType: args.Wallet.GetType(), @@ -127,90 +126,29 @@ func (a *arkClient) InitWithWallet( Dust: info.Dust, BoardingDescriptorTemplate: info.BoardingDescriptorTemplate, } - if err := a.store.AddData(ctx, storeData); err != nil { + if err := a.sdkRepository.ConfigRepository().AddData(ctx, storeData); err != nil { return err } if _, err := args.Wallet.Create(ctx, args.Password, args.Seed); err != nil { //nolint:all - a.store.CleanData(ctx) + a.sdkRepository.ConfigRepository().CleanData(ctx) return err } - a.StoreData = &storeData + a.ConfigData = &storeData a.wallet = args.Wallet a.explorer = explorerSvc a.client = clientSvc - - a.vtxosChan = make(chan map[spent]client.Vtxo) - a.listenForVtxos(ctx, a.vtxosChan) + a.sdkInitialized = true return nil } func (a *arkClient) GetTransactionHistory( ctx context.Context, -) ([]store.Transaction, error) { - return a.appDataStore.TransactionRepository().GetAll(ctx) -} - -func (a *arkClient) ListVtxos( - ctx context.Context, -) (spendableVtxos, spentVtxos []client.Vtxo, err error) { - offchainAddrs, _, _, err := a.wallet.GetAddresses(ctx) - if err != nil { - return - } - - for _, addr := range offchainAddrs { - spendable, spent, err := a.client.ListVtxos(ctx, addr) - if err != nil { - return nil, nil, err - } - spendableVtxos = append(spendableVtxos, spendable...) - spentVtxos = append(spentVtxos, spent...) - } - - return -} - -func (a *arkClient) listenForVtxos( - ctx context.Context, - vtxoChan chan<- map[spent]client.Vtxo, -) { - go func() { - ticker := time.NewTicker(1 * time.Second) - defer ticker.Stop() - - for { - select { - case <-ctx.Done(): - return - case <-ticker.C: - spendableVtxos, spentVtxos, err := a.ListVtxos(ctx) - if err != nil { - log.Warnf("listenForNewVtxos: failed to list vtxos: %s", err) - continue - } - - allVtxos := make(map[spent]client.Vtxo) - for _, vtxo := range spendableVtxos { - allVtxos[vtxoUnspent] = vtxo - } - for _, vtxo := range spentVtxos { - allVtxos[vtxoSpent] = vtxo - } - - go func() { - if len(allVtxos) > 0 { - vtxoChan <- allVtxos - } - }() - } - } - }() - - a.vtxoListeningStarted = true +) ([]domain.Transaction, error) { + return a.sdkRepository.AppDataRepository().TransactionRepository().GetAll(ctx) } func (a *arkClient) Init( @@ -248,7 +186,7 @@ func (a *arkClient) Init( return fmt.Errorf("failed to parse asp pubkey: %s", err) } - storeData := store.StoreData{ + storeData := domain.ConfigData{ AspUrl: args.AspUrl, AspPubkey: aspPubkey, WalletType: args.WalletType, @@ -261,28 +199,26 @@ func (a *arkClient) Init( BoardingDescriptorTemplate: info.BoardingDescriptorTemplate, ExplorerURL: args.ExplorerURL, } - walletSvc, err := getWallet(a.store, &storeData, supportedWallets) + walletSvc, err := getWallet(a.sdkRepository.ConfigRepository(), &storeData, supportedWallets) if err != nil { return err } - if err := a.store.AddData(ctx, storeData); err != nil { + if err := a.sdkRepository.ConfigRepository().AddData(ctx, storeData); err != nil { return err } if _, err := walletSvc.Create(ctx, args.Password, args.Seed); err != nil { //nolint:all - a.store.CleanData(ctx) + a.sdkRepository.ConfigRepository().CleanData(ctx) return err } - a.StoreData = &storeData + a.ConfigData = &storeData a.wallet = walletSvc a.explorer = explorerSvc a.client = clientSvc - - a.vtxosChan = make(chan map[spent]client.Vtxo) - a.listenForVtxos(ctx, a.vtxosChan) + a.sdkInitialized = true return nil } @@ -320,11 +256,11 @@ func (a *arkClient) ping( go func(t *time.Ticker) { if _, err := a.client.Ping(ctx, paymentID); err != nil { - logrus.Warnf("failed to ping asp: %s", err) + log.Warnf("failed to ping asp: %s", err) } for range t.C { if _, err := a.client.Ping(ctx, paymentID); err != nil { - logrus.Warnf("failed to ping asp: %s", err) + log.Warnf("failed to ping asp: %s", err) } } }(ticker) @@ -350,7 +286,7 @@ func getExplorer(explorerURL, network string) (explorer.Explorer, error) { } func getWallet( - storeSvc store.ConfigStore, data *store.StoreData, supportedWallets utils.SupportedType[struct{}], + storeSvc domain.ConfigRepository, data *domain.ConfigData, supportedWallets utils.SupportedType[struct{}], ) (wallet.WalletService, error) { switch data.WalletType { case wallet.SingleKeyWallet: @@ -364,7 +300,7 @@ func getWallet( } func getSingleKeyWallet( - configStore store.ConfigStore, network string, + configStore domain.ConfigRepository, network string, ) (wallet.WalletService, error) { walletStore, err := getWalletStore(configStore.GetType(), configStore.GetDatadir()) if err != nil { @@ -390,3 +326,55 @@ func getWalletStore(storeType, datadir string) (walletstore.WalletStore, error) func getCreatedAtFromExpiry(roundLifetime int64, expiry time.Time) time.Time { return expiry.Add(-time.Duration(roundLifetime) * time.Second) } + +func getNewVtxos( + newVtxosMap map[string]client.Vtxo, + oldVtxosMap map[string]domain.Vtxo, +) []client.Vtxo { + newVtxos := make([]client.Vtxo, 0) + for key, vtxo := range newVtxosMap { + if _, ok := oldVtxosMap[key]; !ok { + newVtxos = append(newVtxos, vtxo) + } + } + return newVtxos +} + +func filterNewBoardingTxs( + allBoardingTxs []domain.Transaction, + oldBoardingTxs []domain.Transaction, +) []domain.Transaction { + newBoardingTxs := make([]domain.Transaction, 0) + for _, tx := range allBoardingTxs { + found := false + for _, oldTx := range oldBoardingTxs { + if tx.BoardingTxid == oldTx.BoardingTxid { + found = true + break + } + } + if !found { + newBoardingTxs = append(newBoardingTxs, tx) + } + } + return newBoardingTxs +} + +func convertVtxosToDomainVtxos(vtxos []client.Vtxo, spent bool) []domain.Vtxo { + domainVtxos := make([]domain.Vtxo, len(vtxos)) + for i, v := range vtxos { + domainVtxos[i] = domain.Vtxo{ + Txid: v.Txid, + VOut: v.VOut, + Amount: v.Amount, + RoundTxid: v.RoundTxid, + ExpiresAt: v.ExpiresAt, + RedeemTx: v.RedeemTx, + UnconditionalForfeitTxs: v.UnconditionalForfeitTxs, + Pending: v.Pending, + SpentBy: v.SpentBy, + Spent: spent, + } + } + return domainVtxos +} diff --git a/pkg/client-sdk/client_test.go b/pkg/client-sdk/client_test.go index 1d2efe318..7630527f8 100644 --- a/pkg/client-sdk/client_test.go +++ b/pkg/client-sdk/client_test.go @@ -8,7 +8,7 @@ import ( "time" "github.com/ark-network/ark/pkg/client-sdk/client" - "github.com/ark-network/ark/pkg/client-sdk/store" + "github.com/ark-network/ark/pkg/client-sdk/store/domain" "github.com/stretchr/testify/require" ) @@ -16,20 +16,20 @@ func TestVtxosToTxsCovenant(t *testing.T) { tests := []struct { name string fixture string - want []store.Transaction + want []domain.Transaction }{ { name: "Alice Sends to Bob", fixture: aliceToBobCovenant, - want: []store.Transaction{ + want: []domain.Transaction{ { Amount: 100000000, - Type: store.TxReceived, + Type: domain.TxReceived, IsPending: false, }, { Amount: 20000, - Type: store.TxSent, + Type: domain.TxSent, IsPending: false, }, }, @@ -63,16 +63,16 @@ func TestVtxosToTxsCovenantless(t *testing.T) { tests := []struct { name string fixture string - want []store.Transaction + want []domain.Transaction }{ { name: "Alice Before Sending Async", fixture: aliceBeforeSendingAsync, - want: []store.Transaction{ + want: []domain.Transaction{ { RoundTxid: "377fa2fbd27c82bdbc095478384c88b6c75432c0ef464189e49c965194446cdf", Amount: 20000, - Type: store.TxReceived, + Type: domain.TxReceived, IsPending: false, CreatedAt: time.Unix(1726054898, 0), }, @@ -81,18 +81,18 @@ func TestVtxosToTxsCovenantless(t *testing.T) { { name: "Alice After Sending Async", fixture: aliceAfterSendingAsync, - want: []store.Transaction{ + want: []domain.Transaction{ { RoundTxid: "377fa2fbd27c82bdbc095478384c88b6c75432c0ef464189e49c965194446cdf", Amount: 20000, - Type: store.TxReceived, + Type: domain.TxReceived, IsPending: false, CreatedAt: time.Unix(1726054898, 0), }, { RedeemTxid: "94fa598302f17f00c8881e742ec0ce2f8c8d16f3d54fe6ba0fb7d13a493d84ad", Amount: 1000, - Type: store.TxSent, + Type: domain.TxSent, IsPending: true, CreatedAt: time.Unix(1726054898, 0), }, @@ -101,18 +101,18 @@ func TestVtxosToTxsCovenantless(t *testing.T) { { name: "Bob Before Claiming Async", fixture: bobBeforeClaimingAsync, - want: []store.Transaction{ + want: []domain.Transaction{ { RedeemTxid: "766fc46ba5c2da41cd4c4bc0566e0f4e0f24c184c41acd3bead5cd7b11120367", Amount: 2000, - Type: store.TxReceived, + Type: domain.TxReceived, IsPending: true, CreatedAt: time.Unix(1726486359, 0), }, { RedeemTxid: "94fa598302f17f00c8881e742ec0ce2f8c8d16f3d54fe6ba0fb7d13a493d84ad", Amount: 1000, - Type: store.TxReceived, + Type: domain.TxReceived, IsPending: true, CreatedAt: time.Unix(1726054898, 0), }, @@ -121,18 +121,18 @@ func TestVtxosToTxsCovenantless(t *testing.T) { { name: "Bob After Claiming Async", fixture: bobAfterClaimingAsync, - want: []store.Transaction{ + want: []domain.Transaction{ { RedeemTxid: "766fc46ba5c2da41cd4c4bc0566e0f4e0f24c184c41acd3bead5cd7b11120367", Amount: 2000, - Type: store.TxReceived, + Type: domain.TxReceived, IsPending: false, CreatedAt: time.Unix(1726486359, 0), }, { RedeemTxid: "94fa598302f17f00c8881e742ec0ce2f8c8d16f3d54fe6ba0fb7d13a493d84ad", Amount: 1000, - Type: store.TxReceived, + Type: domain.TxReceived, IsPending: false, CreatedAt: time.Unix(1726054898, 0), }, @@ -141,25 +141,25 @@ func TestVtxosToTxsCovenantless(t *testing.T) { { name: "Bob After Sending Async", fixture: bobAfterSendingAsync, - want: []store.Transaction{ + want: []domain.Transaction{ { RedeemTxid: "23c3a885f0ea05f7bdf83f3bf7f8ac9dc3f791ad292f4e63a6f53fa5e4935ab0", Amount: 2100, - Type: store.TxSent, + Type: domain.TxSent, IsPending: true, CreatedAt: time.Unix(1726503865, 0), }, { RedeemTxid: "766fc46ba5c2da41cd4c4bc0566e0f4e0f24c184c41acd3bead5cd7b11120367", Amount: 2000, - Type: store.TxReceived, + Type: domain.TxReceived, IsPending: false, CreatedAt: time.Unix(1726486359, 0), }, { RedeemTxid: "94fa598302f17f00c8881e742ec0ce2f8c8d16f3d54fe6ba0fb7d13a493d84ad", Amount: 1000, - Type: store.TxReceived, + Type: domain.TxReceived, IsPending: false, CreatedAt: time.Unix(1726054898, 0), }, @@ -195,16 +195,16 @@ type vtxos struct { spent []client.Vtxo } -func loadFixtures(jsonStr string) (vtxos, []store.Transaction, error) { +func loadFixtures(jsonStr string) (vtxos, []domain.Transaction, error) { var data struct { BoardingTxs []struct { - BoardingTxid string `json:"boardingTxid"` - RoundTxid string `json:"roundTxid"` - Amount uint64 `json:"amount"` - Type store.TxType `json:"txType"` - Pending bool `json:"pending"` - Claimed bool `json:"claimed"` - CreatedAt string `json:"createdAt"` + BoardingTxid string `json:"boardingTxid"` + RoundTxid string `json:"roundTxid"` + Amount uint64 `json:"amount"` + Type domain.TxType `json:"txType"` + Pending bool `json:"pending"` + Claimed bool `json:"claimed"` + CreatedAt string `json:"createdAt"` } `json:"boardingTxs"` SpendableVtxos []struct { Outpoint struct { @@ -302,17 +302,17 @@ func loadFixtures(jsonStr string) (vtxos, []store.Transaction, error) { } } - boardingTxs := make([]store.Transaction, len(data.BoardingTxs)) + boardingTxs := make([]domain.Transaction, len(data.BoardingTxs)) for i, tx := range data.BoardingTxs { createdAt, err := parseTimestamp(tx.CreatedAt) if err != nil { return vtxos{}, nil, err } - boardingTxs[i] = store.Transaction{ + boardingTxs[i] = domain.Transaction{ BoardingTxid: tx.BoardingTxid, RoundTxid: tx.RoundTxid, Amount: tx.Amount, - Type: store.TxReceived, + Type: domain.TxReceived, IsPending: tx.Pending, CreatedAt: createdAt, } diff --git a/pkg/client-sdk/covenant_client.go b/pkg/client-sdk/covenant_client.go index 2014b95fd..0ec848355 100644 --- a/pkg/client-sdk/covenant_client.go +++ b/pkg/client-sdk/covenant_client.go @@ -17,7 +17,7 @@ import ( "github.com/ark-network/ark/pkg/client-sdk/explorer" "github.com/ark-network/ark/pkg/client-sdk/internal/utils" "github.com/ark-network/ark/pkg/client-sdk/redemption" - "github.com/ark-network/ark/pkg/client-sdk/store" + "github.com/ark-network/ark/pkg/client-sdk/store/domain" "github.com/ark-network/ark/pkg/client-sdk/wallet" "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/chaincfg/chainhash" @@ -56,10 +56,9 @@ type covenantArkClient struct { } func NewCovenantClient( - storeSvc store.ConfigStore, - appDataStore store.AppDataStore, + sdkRepository domain.SdkRepository, ) (ArkClient, error) { - data, err := storeSvc.GetData(context.Background()) + data, err := sdkRepository.ConfigRepository().GetData(context.Background()) if err != nil { return nil, err } @@ -69,8 +68,7 @@ func NewCovenantClient( cvnt := &covenantArkClient{ arkClient: &arkClient{ - store: storeSvc, - appDataStore: appDataStore, + sdkRepository: sdkRepository, }, } @@ -82,14 +80,13 @@ func NewCovenantClient( } func LoadCovenantClient( - storeSvc store.ConfigStore, - appDataStore store.AppDataStore, + sdkRepository domain.SdkRepository, ) (ArkClient, error) { - if storeSvc == nil { - return nil, fmt.Errorf("missin store service") + if sdkRepository == nil { + return nil, fmt.Errorf("missing sdk repository") } - data, err := storeSvc.GetData(context.Background()) + data, err := sdkRepository.ConfigRepository().GetData(context.Background()) if err != nil { return nil, err } @@ -109,19 +106,18 @@ func LoadCovenantClient( return nil, fmt.Errorf("failed to setup explorer: %s", err) } - walletSvc, err := getWallet(storeSvc, data, supportedWallets) + walletSvc, err := getWallet(sdkRepository.ConfigRepository(), data, supportedWallets) if err != nil { return nil, fmt.Errorf("faile to setup wallet: %s", err) } cvnt := &covenantArkClient{ &arkClient{ - StoreData: data, - wallet: walletSvc, - store: storeSvc, - appDataStore: appDataStore, - explorer: explorerSvc, - client: clientSvc, + ConfigData: data, + wallet: walletSvc, + sdkRepository: sdkRepository, + explorer: explorerSvc, + client: clientSvc, }, } @@ -133,18 +129,17 @@ func LoadCovenantClient( } func LoadCovenantClientWithWallet( - storeSvc store.ConfigStore, - appDataStore store.AppDataStore, + sdkRepository domain.SdkRepository, walletSvc wallet.WalletService, ) (ArkClient, error) { - if storeSvc == nil { - return nil, fmt.Errorf("missin store service") + if sdkRepository == nil { + return nil, fmt.Errorf("missing sdk repository") } if walletSvc == nil { return nil, fmt.Errorf("missin wallet service") } - data, err := storeSvc.GetData(context.Background()) + data, err := sdkRepository.ConfigRepository().GetData(context.Background()) if err != nil { return nil, err } @@ -166,12 +161,11 @@ func LoadCovenantClientWithWallet( cvnt := &covenantArkClient{ &arkClient{ - StoreData: data, - wallet: walletSvc, - store: storeSvc, - appDataStore: appDataStore, - explorer: explorerSvc, - client: clientSvc, + ConfigData: data, + wallet: walletSvc, + sdkRepository: sdkRepository, + explorer: explorerSvc, + client: clientSvc, }, } @@ -593,184 +587,164 @@ func (a *covenantArkClient) Claim(ctx context.Context) (string, error) { ) } -func (a *covenantArkClient) GetTransactionEventChannel() chan store.Transaction { - return a.appDataStore.TransactionRepository().GetEventChannel() +func (a *covenantArkClient) GetTransactionEventChannel() chan domain.Transaction { + return a.sdkRepository.AppDataRepository().TransactionRepository().GetEventChannel() } func (a *covenantArkClient) listenToVtxoChan(ctnx context.Context) error { go func(ctx context.Context) { + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + for { - if !a.vtxoListeningStarted { - time.Sleep(1 * time.Second) + if !a.sdkInitialized { continue } select { case <-ctx.Done(): return - case allVtxow := <-a.vtxosChan: - if len(allVtxow) == 0 { + case <-ticker.C: + allSpendableVtxos, allSpentVtxos, err := a.ListVtxos(ctx) + if err != nil { + log.Errorf("failed to list vtxos: %s", err) continue } - spendableVtxosOld, spentVtxosOld, err := a.appDataStore.VtxoRepository().GetAll(ctx) + spendableVtxosOld, spentVtxosOld, err := a.sdkRepository. + AppDataRepository().VtxoRepository().GetAll(ctx) if err != nil { log.Errorf("failed to get vtxos: %s", err) continue } + allBoardingTxs := a.getBoardingTxs(ctx) oldVtxos := append(spendableVtxosOld, spentVtxosOld...) + if len(oldVtxos) == 0 { - spendableVtxos, spentVtxos := make([]client.Vtxo, 0), make([]client.Vtxo, 0) - for isSpent, vtxo := range allVtxow { - if isSpent { - spentVtxos = append(spentVtxos, vtxo) - } else { - spendableVtxos = append(spendableVtxos, vtxo) - } + err = a.insertInitialVtxosAndTransactions( + ctx, + allSpendableVtxos, + allSpentVtxos, + allBoardingTxs, + ) + if err != nil { + log.Errorf("failed to process new vtxos: %s", err) } - - txs, err := vtxosToTxsCovenant(a.StoreData.RoundInterval, spendableVtxos, spentVtxos, allBoardingTxs) + } else { + err := a.insertNewTxsAndTransactions( + ctx, + allSpendableVtxos, + allSpentVtxos, + oldVtxos, + allBoardingTxs, + ) if err != nil { - log.Errorf("failed to convert vtxos to txs: %s", err) - continue + log.Errorf("failed to update vtxos: %s", err) } + } + } + } + }(ctnx) - if len(txs) > 0 { - if err := a.appDataStore.TransactionRepository().InsertTransactions(ctx, txs); err != nil { - log.Errorf("failed to insert transaction: %s", err) - continue - } - } + return nil +} - vtxos := make([]store.Vtxo, 0, len(allVtxow)) - for isSpent, v := range allVtxow { - vtxos = append(vtxos, store.Vtxo{ - Txid: v.Txid, - VOut: v.VOut, - Amount: v.Amount, - RoundTxid: v.RoundTxid, - ExpiresAt: v.ExpiresAt, - RedeemTx: v.RedeemTx, - UnconditionalForfeitTxs: v.UnconditionalForfeitTxs, - Pending: v.Pending, - SpentBy: v.SpentBy, - Spent: bool(isSpent), - }) - if len(vtxos) > 0 { - if err := a.appDataStore.VtxoRepository().InsertVtxos(ctx, vtxos); err != nil { - log.Errorf("failed to insert vtxo: %s", err) - continue - } - } - } - } else { - // find differece between old and new vtxos - allVtxosMap := make(map[string]client.Vtxo, 0) - for _, vtxo := range allVtxow { - key := fmt.Sprintf("%s:%d", vtxo.Txid, vtxo.VOut) - allVtxosMap[key] = vtxo - } +func (a *covenantArkClient) insertInitialVtxosAndTransactions( + ctx context.Context, + spendableVtxos, + spentVtxos []client.Vtxo, + boardingTxs []domain.Transaction, +) error { + txs, err := vtxosToTxsCovenant( + a.ConfigData.RoundInterval, + spendableVtxos, + spentVtxos, + boardingTxs, + ) + if err != nil { + return err + } - oldVtxosMap := make(map[string]store.Vtxo, 0) - for _, vtxo := range oldVtxos { - key := fmt.Sprintf("%s:%d", vtxo.Txid, vtxo.VOut) - oldVtxosMap[key] = vtxo - } + if len(txs) > 0 { + if err := a.sdkRepository.AppDataRepository().TransactionRepository(). + InsertTransactions(ctx, txs); err != nil { + return err + } + } - // find new vtxos - newSpendableVtxos, newSpentVtxos := make([]client.Vtxo, 0), make([]client.Vtxo, 0) - for isSpent, vtxo := range allVtxow { - key := fmt.Sprintf("%s:%d", vtxo.Txid, vtxo.VOut) - if _, ok := oldVtxosMap[key]; !ok { - if isSpent { - newSpentVtxos = append(newSpentVtxos, vtxo) - } else { - newSpendableVtxos = append(newSpendableVtxos, vtxo) - } - } - } + domainVtxos := convertVtxosToDomainVtxos(spendableVtxos, false) + domainVtxos = append(domainVtxos, convertVtxosToDomainVtxos(spentVtxos, true)...) - oldBardingTxs, err := a.appDataStore.TransactionRepository().GetBoardingTxs(ctx) - if err != nil { - log.Errorf("failed to get boarding txs: %s", err) - continue - } + if len(domainVtxos) > 0 { + return a.sdkRepository.AppDataRepository().VtxoRepository(). + InsertVtxos(ctx, domainVtxos) + } - newBoardingTxs := make([]store.Transaction, 0) - if len(oldBardingTxs) > 0 { - for _, tx := range allBoardingTxs { - found := false - for _, oldTx := range oldBardingTxs { - if tx.BoardingTxid == oldTx.BoardingTxid { - found = true - break - } - } - if !found { - newBoardingTxs = append(newBoardingTxs, tx) - } - } - } + return nil +} - txs, err := vtxosToTxsCovenant(a.StoreData.RoundInterval, newSpendableVtxos, newSpentVtxos, newBoardingTxs) - if err != nil { - log.Errorf("failed to convert vtxos to txs: %s", err) - continue - } +func (a *covenantArkClient) insertNewTxsAndTransactions( + ctx context.Context, + allSpendableVtxos, + allSpentVtxos []client.Vtxo, + oldVtxos []domain.Vtxo, + allBoardingTxs []domain.Transaction, +) error { + spendableVtxosMap := make(map[string]client.Vtxo) + spentVtxosMap := make(map[string]client.Vtxo) + oldVtxosMap := make(map[string]domain.Vtxo) - if len(txs) > 0 { - if err := a.appDataStore.TransactionRepository().InsertTransactions(ctx, txs); err != nil { - log.Errorf("failed to insert transaction: %s", err) - continue - } - } + for _, vtxo := range allSpendableVtxos { + key := fmt.Sprintf("%s:%d", vtxo.Txid, vtxo.VOut) + spendableVtxosMap[key] = vtxo + } - spendableVtxosToInsert := make([]store.Vtxo, 0, len(newSpendableVtxos)) - for _, v := range newSpendableVtxos { - spendableVtxosToInsert = append(spendableVtxosToInsert, store.Vtxo{ - Txid: v.Txid, - VOut: v.VOut, - Amount: v.Amount, - RoundTxid: v.RoundTxid, - ExpiresAt: v.ExpiresAt, - RedeemTx: v.RedeemTx, - UnconditionalForfeitTxs: v.UnconditionalForfeitTxs, - Pending: v.Pending, - SpentBy: v.SpentBy, - Spent: false, - }) - } + for _, vtxo := range allSpentVtxos { + key := fmt.Sprintf("%s:%d", vtxo.Txid, vtxo.VOut) + spentVtxosMap[key] = vtxo + } - spentVtxosToInsert := make([]store.Vtxo, 0, len(newSpentVtxos)) - for _, v := range newSpentVtxos { - spentVtxosToInsert = append(spentVtxosToInsert, store.Vtxo{ - Txid: v.Txid, - VOut: v.VOut, - Amount: v.Amount, - RoundTxid: v.RoundTxid, - ExpiresAt: v.ExpiresAt, - RedeemTx: v.RedeemTx, - UnconditionalForfeitTxs: v.UnconditionalForfeitTxs, - Pending: v.Pending, - SpentBy: v.SpentBy, - Spent: true, - }) - } + for _, vtxo := range oldVtxos { + key := fmt.Sprintf("%s:%d", vtxo.Txid, vtxo.VOut) + oldVtxosMap[key] = vtxo + } - if len(spendableVtxosToInsert) > 0 || len(spentVtxosToInsert) > 0 { - if err := a.appDataStore.VtxoRepository().InsertVtxos( - ctx, - append(spentVtxosToInsert, spendableVtxosToInsert...), - ); err != nil { - log.Errorf("failed to insert vtxo: %s", err) - continue - } - } - } - } + newSpendableVtxos := getNewVtxos(spendableVtxosMap, oldVtxosMap) + newSpentVtxos := getNewVtxos(spentVtxosMap, oldVtxosMap) + + oldBoardingTxs, err := a.sdkRepository.AppDataRepository(). + TransactionRepository().GetBoardingTxs(ctx) + if err != nil { + return err + } + + newBoardingTxs := filterNewBoardingTxs(allBoardingTxs, oldBoardingTxs) + + txs, err := vtxosToTxsCovenant( + a.ConfigData.RoundInterval, + newSpendableVtxos, + newSpentVtxos, + newBoardingTxs, + ) + if err != nil { + return err + } + + if len(txs) > 0 { + if err := a.sdkRepository.AppDataRepository().TransactionRepository(). + InsertTransactions(ctx, txs); err != nil { + return err } - }(ctnx) + } + + domainSpendableVtxos := convertVtxosToDomainVtxos(newSpendableVtxos, false) + domainSpentVtxos := convertVtxosToDomainVtxos(newSpentVtxos, true) + + if len(domainSpendableVtxos) > 0 || len(domainSpentVtxos) > 0 { + return a.sdkRepository.AppDataRepository().VtxoRepository(). + InsertVtxos(ctx, append(domainSpentVtxos, domainSpendableVtxos...)) + } return nil } @@ -1393,7 +1367,7 @@ func (a *covenantArkClient) validateCongestionTree( if !utils.IsOnchainOnly(receivers) { if err := tree.ValidateCongestionTree( - event.Tree, poolTx, a.StoreData.AspPubkey, a.RoundLifetime, + event.Tree, poolTx, a.ConfigData.AspPubkey, a.RoundLifetime, ); err != nil { return err } @@ -1871,7 +1845,7 @@ func (a *covenantArkClient) offchainAddressToDefaultVtxoDescriptor(addr string) return vtxoScript.ToDescriptor(), nil } -func (a *covenantArkClient) getBoardingTxs(ctx context.Context) (transactions []store.Transaction) { +func (a *covenantArkClient) getBoardingTxs(ctx context.Context) (transactions []domain.Transaction) { utxos, err := a.getClaimableBoardingUtxos(ctx) if err != nil { return nil @@ -1888,10 +1862,10 @@ func (a *covenantArkClient) getBoardingTxs(ctx context.Context) (transactions [] } for _, u := range allUtxos { - transactions = append(transactions, store.Transaction{ + transactions = append(transactions, domain.Transaction{ BoardingTxid: u.Txid, Amount: u.Amount, - Type: store.TxReceived, + Type: domain.TxReceived, CreatedAt: u.CreatedAt, }) } @@ -1899,10 +1873,10 @@ func (a *covenantArkClient) getBoardingTxs(ctx context.Context) (transactions [] } func vtxosToTxsCovenant( - roundLifetime int64, spendable, spent []client.Vtxo, boardingTxs []store.Transaction, -) ([]store.Transaction, error) { - transactions := make([]store.Transaction, 0) - unconfirmedBoardingTxs := make([]store.Transaction, 0) + roundLifetime int64, spendable, spent []client.Vtxo, boardingTxs []domain.Transaction, +) ([]domain.Transaction, error) { + transactions := make([]domain.Transaction, 0) + unconfirmedBoardingTxs := make([]domain.Transaction, 0) for _, tx := range boardingTxs { emptyTime := time.Time{} if tx.CreatedAt == emptyTime { @@ -1927,9 +1901,9 @@ func vtxosToTxsCovenant( } } // what kind of tx was this? send or receive? - txType := store.TxReceived + txType := domain.TxReceived if amount < 0 { - txType = store.TxSent + txType = domain.TxSent } // get redeem txid redeemTxid := "" @@ -1941,7 +1915,7 @@ func vtxosToTxsCovenant( redeemTxid = txid } // add transaction - transactions = append(transactions, store.Transaction{ + transactions = append(transactions, domain.Transaction{ RoundTxid: v.RoundTxid, RedeemTxid: redeemTxid, Amount: uint64(math.Abs(float64(amount))), diff --git a/pkg/client-sdk/covenantless_client.go b/pkg/client-sdk/covenantless_client.go index 8119d8f3e..0933a1db9 100644 --- a/pkg/client-sdk/covenantless_client.go +++ b/pkg/client-sdk/covenantless_client.go @@ -18,7 +18,7 @@ import ( "github.com/ark-network/ark/pkg/client-sdk/explorer" "github.com/ark-network/ark/pkg/client-sdk/internal/utils" "github.com/ark-network/ark/pkg/client-sdk/redemption" - "github.com/ark-network/ark/pkg/client-sdk/store" + "github.com/ark-network/ark/pkg/client-sdk/store/domain" "github.com/ark-network/ark/pkg/client-sdk/wallet" "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcutil" @@ -58,10 +58,9 @@ type covenantlessArkClient struct { } func NewCovenantlessClient( - storeSvc store.ConfigStore, - appDataStore store.AppDataStore, + sdkRepository domain.SdkRepository, ) (ArkClient, error) { - data, err := storeSvc.GetData(context.Background()) + data, err := sdkRepository.ConfigRepository().GetData(context.Background()) if err != nil { return nil, err } @@ -71,8 +70,7 @@ func NewCovenantlessClient( cvnt := &covenantlessArkClient{ arkClient: &arkClient{ - store: storeSvc, - appDataStore: appDataStore, + sdkRepository: sdkRepository, }, } @@ -84,14 +82,13 @@ func NewCovenantlessClient( } func LoadCovenantlessClient( - storeSvc store.ConfigStore, - appDataStore store.AppDataStore, + sdkRepository domain.SdkRepository, ) (ArkClient, error) { - if storeSvc == nil { - return nil, fmt.Errorf("missin store service") + if sdkRepository == nil { + return nil, fmt.Errorf("missing sdk repository") } - data, err := storeSvc.GetData(context.Background()) + data, err := sdkRepository.ConfigRepository().GetData(context.Background()) if err != nil { return nil, err } @@ -111,19 +108,18 @@ func LoadCovenantlessClient( return nil, fmt.Errorf("failed to setup explorer: %s", err) } - walletSvc, err := getWallet(storeSvc, data, supportedWallets) + walletSvc, err := getWallet(sdkRepository.ConfigRepository(), data, supportedWallets) if err != nil { return nil, fmt.Errorf("faile to setup wallet: %s", err) } cvnt := &covenantlessArkClient{ &arkClient{ - StoreData: data, - wallet: walletSvc, - store: storeSvc, - appDataStore: appDataStore, - explorer: explorerSvc, - client: clientSvc, + ConfigData: data, + wallet: walletSvc, + sdkRepository: sdkRepository, + explorer: explorerSvc, + client: clientSvc, }, } @@ -135,18 +131,17 @@ func LoadCovenantlessClient( } func LoadCovenantlessClientWithWallet( - storeSvc store.ConfigStore, + sdkRepository domain.SdkRepository, walletSvc wallet.WalletService, - appDataStore store.AppDataStore, ) (ArkClient, error) { - if storeSvc == nil { - return nil, fmt.Errorf("missin store service") + if sdkRepository == nil { + return nil, fmt.Errorf("missin sdk repository") } if walletSvc == nil { return nil, fmt.Errorf("missin wallet service") } - data, err := storeSvc.GetData(context.Background()) + data, err := sdkRepository.ConfigRepository().GetData(context.Background()) if err != nil { return nil, err } @@ -168,12 +163,11 @@ func LoadCovenantlessClientWithWallet( cvnt := &covenantlessArkClient{ &arkClient{ - StoreData: data, - wallet: walletSvc, - store: storeSvc, - appDataStore: appDataStore, - explorer: explorerSvc, - client: clientSvc, + ConfigData: data, + wallet: walletSvc, + sdkRepository: sdkRepository, + explorer: explorerSvc, + client: clientSvc, }, } @@ -771,175 +765,164 @@ func (a *covenantlessArkClient) Claim(ctx context.Context) (string, error) { ) } -func (a *covenantlessArkClient) GetTransactionEventChannel() chan store.Transaction { - return a.appDataStore.TransactionRepository().GetEventChannel() +func (a *covenantlessArkClient) GetTransactionEventChannel() chan domain.Transaction { + return a.sdkRepository.AppDataRepository().TransactionRepository().GetEventChannel() } func (a *covenantlessArkClient) listenToVtxoChan(ctnx context.Context) error { go func(ctx context.Context) { + ticker := time.NewTicker(1 * time.Second) + defer ticker.Stop() + for { - if !a.vtxoListeningStarted { - time.Sleep(1 * time.Second) + if !a.sdkInitialized { continue } select { case <-ctx.Done(): return - case allVtxow := <-a.vtxosChan: - if len(allVtxow) == 0 { + case <-ticker.C: + allSpendableVtxos, allSpentVtxos, err := a.ListVtxos(ctx) + if err != nil { + log.Errorf("failed to list vtxos: %s", err) continue } - spendableVtxosOld, spentVtxosOld, err := a.appDataStore.VtxoRepository().GetAll(ctx) + spendableVtxosOld, spentVtxosOld, err := a.sdkRepository. + AppDataRepository().VtxoRepository().GetAll(ctx) if err != nil { log.Errorf("failed to get vtxos: %s", err) continue } + allBoardingTxs := a.getBoardingTxs(ctx) oldVtxos := append(spendableVtxosOld, spentVtxosOld...) if len(oldVtxos) == 0 { - spendableVtxos, spentVtxos := make([]client.Vtxo, 0), make([]client.Vtxo, 0) - for isSpent, vtxo := range allVtxow { - if isSpent { - spentVtxos = append(spentVtxos, vtxo) - } else { - spendableVtxos = append(spendableVtxos, vtxo) - } + err = a.insertInitialTxsAndVtxos( + ctx, + allSpendableVtxos, + allSpentVtxos, + allBoardingTxs, + ) + if err != nil { + log.Errorf("failed to process new vtxos: %s", err) } - - txs, err := vtxosToTxsCovenantless(a.StoreData.RoundInterval, spendableVtxos, spentVtxos, allBoardingTxs) + } else { + err := a.insertNewTxsAndVtxos( + ctx, + allSpendableVtxos, + allSpentVtxos, + oldVtxos, + allBoardingTxs, + ) if err != nil { - log.Errorf("failed to convert vtxos to txs: %s", err) - continue + log.Errorf("failed to update vtxos: %s", err) } + } + } + } + }(ctnx) - if err := a.appDataStore.TransactionRepository().InsertTransactions(ctx, txs); err != nil { - log.Errorf("failed to insert transaction: %s", err) - continue - } + return nil +} - vtxos := make([]store.Vtxo, 0, len(allVtxow)) - for isSpent, v := range allVtxow { - vtxos = append(vtxos, store.Vtxo{ - Txid: v.Txid, - VOut: v.VOut, - Amount: v.Amount, - RoundTxid: v.RoundTxid, - ExpiresAt: v.ExpiresAt, - RedeemTx: v.RedeemTx, - UnconditionalForfeitTxs: v.UnconditionalForfeitTxs, - Pending: v.Pending, - SpentBy: v.SpentBy, - Spent: bool(isSpent), - }) - if err := a.appDataStore.VtxoRepository().InsertVtxos(ctx, vtxos); err != nil { - log.Errorf("failed to insert vtxo: %s", err) - continue - } - } - } else { - // find differece between old and new vtxos - allVtxosMap := make(map[string]client.Vtxo, 0) - for _, vtxo := range allVtxow { - key := fmt.Sprintf("%s:%d", vtxo.Txid, vtxo.VOut) - allVtxosMap[key] = vtxo - } +func (a *covenantlessArkClient) insertInitialTxsAndVtxos( + ctx context.Context, + spendableVtxos, + spentVtxos []client.Vtxo, + boardingTxs []domain.Transaction, +) error { + txs, err := vtxosToTxsCovenantless( + a.ConfigData.RoundInterval, + spendableVtxos, + spentVtxos, + boardingTxs, + ) + if err != nil { + return err + } - oldVtxosMap := make(map[string]store.Vtxo, 0) - for _, vtxo := range oldVtxos { - key := fmt.Sprintf("%s:%d", vtxo.Txid, vtxo.VOut) - oldVtxosMap[key] = vtxo - } + if len(txs) > 0 { + if err := a.sdkRepository.AppDataRepository().TransactionRepository(). + InsertTransactions(ctx, txs); err != nil { + return err + } + } - // find new vtxos - newSpendableVtxos, newSpentVtxos := make([]client.Vtxo, 0), make([]client.Vtxo, 0) - for isSpent, vtxo := range allVtxow { - key := fmt.Sprintf("%s:%d", vtxo.Txid, vtxo.VOut) - if _, ok := oldVtxosMap[key]; !ok { - if isSpent { - newSpentVtxos = append(newSpentVtxos, vtxo) - } else { - newSpendableVtxos = append(newSpendableVtxos, vtxo) - } - } - } + domainVtxos := append( + convertVtxosToDomainVtxos(spendableVtxos, false), + convertVtxosToDomainVtxos(spentVtxos, true)..., + ) + if len(domainVtxos) > 0 { + return a.sdkRepository.AppDataRepository().VtxoRepository(). + InsertVtxos(ctx, domainVtxos) + } - oldBardingTxs, err := a.appDataStore.TransactionRepository().GetBoardingTxs(ctx) - if err != nil { - log.Errorf("failed to get boarding txs: %s", err) - continue - } + return nil +} - newBoardingTxs := make([]store.Transaction, 0) - if len(oldBardingTxs) > 0 { - for _, tx := range allBoardingTxs { - found := false - for _, oldTx := range oldBardingTxs { - if tx.BoardingTxid == oldTx.BoardingTxid { - found = true - break - } - } - if !found { - newBoardingTxs = append(newBoardingTxs, tx) - } - } - } +func (a *covenantlessArkClient) insertNewTxsAndVtxos( + ctx context.Context, + allSpendableVtxos, + allSpentVtxos []client.Vtxo, + oldVtxos []domain.Vtxo, + allBoardingTxs []domain.Transaction, +) error { + allSpendableVtxosMap := make(map[string]client.Vtxo) + allSpentVtxosMap := make(map[string]client.Vtxo) + oldVtxosMap := make(map[string]domain.Vtxo) - txs, err := vtxosToTxsCovenantless(a.StoreData.RoundInterval, newSpendableVtxos, newSpentVtxos, newBoardingTxs) - if err != nil { - log.Errorf("failed to convert vtxos to txs: %s", err) - continue - } + for _, vtxo := range allSpendableVtxos { + key := fmt.Sprintf("%s:%d", vtxo.Txid, vtxo.VOut) + allSpendableVtxosMap[key] = vtxo + } - if err := a.appDataStore.TransactionRepository().InsertTransactions(ctx, txs); err != nil { - log.Errorf("failed to insert transaction: %s", err) - continue - } + for _, vtxo := range allSpentVtxos { + key := fmt.Sprintf("%s:%d", vtxo.Txid, vtxo.VOut) + allSpentVtxosMap[key] = vtxo + } - spendableVtxosToInsert := make([]store.Vtxo, 0, len(newSpendableVtxos)) - for _, v := range newSpendableVtxos { - spendableVtxosToInsert = append(spendableVtxosToInsert, store.Vtxo{ - Txid: v.Txid, - VOut: v.VOut, - Amount: v.Amount, - RoundTxid: v.RoundTxid, - ExpiresAt: v.ExpiresAt, - RedeemTx: v.RedeemTx, - UnconditionalForfeitTxs: v.UnconditionalForfeitTxs, - Pending: v.Pending, - SpentBy: v.SpentBy, - Spent: false, - }) - } + for _, vtxo := range oldVtxos { + key := fmt.Sprintf("%s:%d", vtxo.Txid, vtxo.VOut) + oldVtxosMap[key] = vtxo + } - spentVtxosToInsert := make([]store.Vtxo, 0, len(newSpentVtxos)) - for _, v := range newSpentVtxos { - spentVtxosToInsert = append(spentVtxosToInsert, store.Vtxo{ - Txid: v.Txid, - VOut: v.VOut, - Amount: v.Amount, - RoundTxid: v.RoundTxid, - ExpiresAt: v.ExpiresAt, - RedeemTx: v.RedeemTx, - UnconditionalForfeitTxs: v.UnconditionalForfeitTxs, - Pending: v.Pending, - SpentBy: v.SpentBy, - Spent: true, - }) - } - if err := a.appDataStore.VtxoRepository().InsertVtxos( - ctx, - append(spentVtxosToInsert, spendableVtxosToInsert...), - ); err != nil { - log.Errorf("failed to insert vtxo: %s", err) - continue - } - } - } + newSpendableVtxos := getNewVtxos(allSpendableVtxosMap, oldVtxosMap) + newSpentVtxos := getNewVtxos(allSpentVtxosMap, oldVtxosMap) + + oldBoardingTxs, err := a.sdkRepository.AppDataRepository(). + TransactionRepository().GetBoardingTxs(ctx) + if err != nil { + return err + } + + newBoardingTxs := filterNewBoardingTxs(allBoardingTxs, oldBoardingTxs) + + txs, err := vtxosToTxsCovenantless( + a.ConfigData.RoundInterval, + newSpendableVtxos, + newSpentVtxos, + newBoardingTxs, + ) + if err != nil { + return err + } + + if len(txs) > 0 { + if err := a.sdkRepository.AppDataRepository().TransactionRepository(). + InsertTransactions(ctx, txs); err != nil { + return err } - }(ctnx) + } + + domainSpendableVtxos := convertVtxosToDomainVtxos(newSpendableVtxos, false) + domainSpentVtxos := convertVtxosToDomainVtxos(newSpentVtxos, true) + + if len(domainSpendableVtxos) > 0 || len(domainSpentVtxos) > 0 { + return a.sdkRepository.AppDataRepository().VtxoRepository(). + InsertVtxos(ctx, append(domainSpentVtxos, domainSpendableVtxos...)) + } return nil } @@ -1584,7 +1567,7 @@ func (a *covenantlessArkClient) validateCongestionTree( if !utils.IsOnchainOnly(receivers) { if err := bitcointree.ValidateCongestionTree( - event.Tree, poolTx, a.StoreData.AspPubkey, a.RoundLifetime, + event.Tree, poolTx, a.ConfigData.AspPubkey, a.RoundLifetime, ); err != nil { return err } @@ -2205,7 +2188,7 @@ func (a *covenantlessArkClient) offchainAddressToDefaultVtxoDescriptor(addr stri return vtxoScript.ToDescriptor(), nil } -func (a *covenantlessArkClient) getBoardingTxs(ctx context.Context) (transactions []store.Transaction) { +func (a *covenantlessArkClient) getBoardingTxs(ctx context.Context) (transactions []domain.Transaction) { utxos, err := a.getClaimableBoardingUtxos(ctx) if err != nil { return nil @@ -2226,10 +2209,10 @@ func (a *covenantlessArkClient) getBoardingTxs(ctx context.Context) (transaction if isPending[u.Txid] { pending = true } - transactions = append(transactions, store.Transaction{ + transactions = append(transactions, domain.Transaction{ BoardingTxid: u.Txid, Amount: u.Amount, - Type: store.TxReceived, + Type: domain.TxReceived, IsPending: pending, CreatedAt: u.CreatedAt, }) @@ -2247,10 +2230,10 @@ func findVtxosBySpentBy(allVtxos []client.Vtxo, txid string) (vtxos []client.Vtx } func vtxosToTxsCovenantless( - roundLifetime int64, spendable, spent []client.Vtxo, boardingTxs []store.Transaction, -) ([]store.Transaction, error) { - transactions := make([]store.Transaction, 0) - unconfirmedBoardingTxs := make([]store.Transaction, 0) + roundLifetime int64, spendable, spent []client.Vtxo, boardingTxs []domain.Transaction, +) ([]domain.Transaction, error) { + transactions := make([]domain.Transaction, 0) + unconfirmedBoardingTxs := make([]domain.Transaction, 0) for _, tx := range boardingTxs { emptyTime := time.Time{} if tx.CreatedAt == emptyTime { @@ -2276,9 +2259,9 @@ func vtxosToTxsCovenantless( } } // what kind of tx was this? send or receive? - txType := store.TxReceived + txType := domain.TxReceived if amount < 0 { - txType = store.TxSent + txType = domain.TxSent } // get redeem txid @@ -2291,7 +2274,7 @@ func vtxosToTxsCovenantless( redeemTxid = txid } // add transaction - transactions = append(transactions, store.Transaction{ + transactions = append(transactions, domain.Transaction{ RoundTxid: v.RoundTxid, RedeemTxid: redeemTxid, Amount: uint64(math.Abs(float64(amount))), diff --git a/pkg/client-sdk/example/covenant/alice_to_bob.go b/pkg/client-sdk/example/covenant/alice_to_bob.go index 49b28f7db..d5062fb92 100644 --- a/pkg/client-sdk/example/covenant/alice_to_bob.go +++ b/pkg/client-sdk/example/covenant/alice_to_bob.go @@ -5,16 +5,14 @@ import ( "fmt" "io" "os/exec" - "path/filepath" - "runtime" + "path" "strings" "sync" "time" "github.com/ark-network/ark/common" arksdk "github.com/ark-network/ark/pkg/client-sdk" - inmemorystore "github.com/ark-network/ark/pkg/client-sdk/store/inmemory" - sqlitestore "github.com/ark-network/ark/pkg/client-sdk/store/sqlite" + "github.com/ark-network/ark/pkg/client-sdk/store" log "github.com/sirupsen/logrus" ) @@ -30,7 +28,7 @@ func main() { log.Info("alice is setting up her ark wallet...") - aliceArkClient, err := setupArkClient() + aliceArkClient, err := setupArkClient("alice") if err != nil { log.Fatal(err) } @@ -76,7 +74,7 @@ func main() { fmt.Println("") log.Info("bob is setting up his ark wallet...") - bobArkClient, err := setupArkClient() + bobArkClient, err := setupArkClient("bob") if err != nil { log.Fatal(err) } @@ -140,22 +138,18 @@ func main() { log.Infof("bob offchain balance: %d", bobBalance.OffchainBalance.Total) } -func setupArkClient() (arksdk.ArkClient, error) { - storeSvc, err := inmemorystore.NewConfigStore() +func setupArkClient(wallet string) (arksdk.ArkClient, error) { + dbDir := common.AppDataDir(path.Join("ark-example", wallet), false) + appDataStore, err := store.NewService(store.Config{ + ConfigStoreType: store.FileStore, + AppDataStoreType: store.Badger, + BaseDir: dbDir, + }) if err != nil { - return nil, fmt.Errorf("failed to setup store: %s", err) + return nil, fmt.Errorf("failed to setup app data store: %s", err) } - dbDir := fmt.Sprintf("%s/%s", common.AppDataDir("ark-example", false), "sqlite") - _, currentFile, _, _ := runtime.Caller(0) - migrationsDir := filepath.Join(filepath.Dir(currentFile), "..", "..", "store", "sqlite", "migration") - appDataStoreMigrationPath := "file://" + migrationsDir - appDataStore, err := sqlitestore.NewAppDataRepository(dbDir, appDataStoreMigrationPath) - if err != nil { - return nil, err - } - - client, err := arksdk.NewCovenantClient(storeSvc, appDataStore) + client, err := arksdk.NewCovenantClient(appDataStore) if err != nil { return nil, fmt.Errorf("failed to setup ark client: %s", err) } diff --git a/pkg/client-sdk/example/covenantless/alice_to_bob.go b/pkg/client-sdk/example/covenantless/alice_to_bob.go index 862b2abe6..8ebaf5840 100644 --- a/pkg/client-sdk/example/covenantless/alice_to_bob.go +++ b/pkg/client-sdk/example/covenantless/alice_to_bob.go @@ -11,8 +11,7 @@ import ( "github.com/ark-network/ark/common" arksdk "github.com/ark-network/ark/pkg/client-sdk" - inmemorystore "github.com/ark-network/ark/pkg/client-sdk/store/inmemory" - sqlitestore "github.com/ark-network/ark/pkg/client-sdk/store/sqlite" + "github.com/ark-network/ark/pkg/client-sdk/store" log "github.com/sirupsen/logrus" ) @@ -144,14 +143,13 @@ func main() { } func setupArkClient() (arksdk.ArkClient, error) { - storeSvc, err := inmemorystore.NewConfigStore() - if err != nil { - return nil, fmt.Errorf("failed to setup store: %s", err) - } - dbDir := fmt.Sprintf("%s/%s", common.AppDataDir("ark-example", false), "sqlite") - appDataStoreMigrationPath := "file://../../pkg/client-sdk/store/sqlite/migrations" - appDataStore, err := sqlitestore.NewAppDataRepository(dbDir, appDataStoreMigrationPath) - client, err := arksdk.NewCovenantlessClient(storeSvc, appDataStore) + dbDir := common.AppDataDir("ark-example", false) + appDataStore, err := store.NewService(store.Config{ + ConfigStoreType: store.FileStore, + AppDataStoreType: store.Badger, + BaseDir: dbDir, + }) + client, err := arksdk.NewCovenantlessClient(appDataStore) if err != nil { return nil, fmt.Errorf("failed to setup ark client: %s", err) } diff --git a/pkg/client-sdk/store/badger/app_data_repository.go b/pkg/client-sdk/store/badger/app_data_repository.go new file mode 100644 index 000000000..4f8ef092c --- /dev/null +++ b/pkg/client-sdk/store/badger/app_data_repository.go @@ -0,0 +1,32 @@ +package badgerstore + +import ( + "github.com/ark-network/ark/pkg/client-sdk/store/domain" +) + +type appDataRepository struct { + transactionRepository domain.TransactionRepository + vtxoRepository domain.VtxoRepository +} + +func NewAppDataRepository( + transactionRepository domain.TransactionRepository, + vtxoRepository domain.VtxoRepository, +) domain.AppDataRepository { + return &appDataRepository{ + transactionRepository: transactionRepository, + vtxoRepository: vtxoRepository, + } +} + +func (a *appDataRepository) TransactionRepository() domain.TransactionRepository { + return a.transactionRepository +} + +func (a *appDataRepository) VtxoRepository() domain.VtxoRepository { + return a.vtxoRepository +} + +func (a *appDataRepository) Stop() { + a.transactionRepository.Stop() +} diff --git a/pkg/client-sdk/store/badger/transaction_repository.go b/pkg/client-sdk/store/badger/transaction_repository.go new file mode 100644 index 000000000..25ce9fa47 --- /dev/null +++ b/pkg/client-sdk/store/badger/transaction_repository.go @@ -0,0 +1,68 @@ +package badgerstore + +import ( + "context" + "fmt" + "path/filepath" + + "github.com/ark-network/ark/pkg/client-sdk/store/domain" + "github.com/dgraph-io/badger/v4" + "github.com/google/uuid" + "github.com/timshannon/badgerhold/v4" +) + +const ( + transactionStoreDir = "transactions" +) + +type transactionRepository struct { + db *badgerhold.Store + eventCh chan domain.Transaction +} + +func NewTransactionRepository( + dir string, logger badger.Logger, +) (domain.TransactionRepository, error) { + badgerDb, err := CreateDB(filepath.Join(dir, transactionStoreDir), logger) + if err != nil { + return nil, fmt.Errorf("failed to open round events store: %s", err) + } + return &transactionRepository{ + db: badgerDb, + eventCh: make(chan domain.Transaction), + }, nil +} + +func (t *transactionRepository) GetBoardingTxs(ctx context.Context) ([]domain.Transaction, error) { + var txs []domain.Transaction + query := badgerhold.Where("BoardingTxid").Ne("") + err := t.db.Find(&txs, query) + return txs, err +} + +func (t *transactionRepository) InsertTransactions(ctx context.Context, txs []domain.Transaction) error { + for _, tx := range txs { + tx.ID = uuid.New().String() + if err := t.db.Insert(tx.ID, &tx); err != nil { + return err + } + go func() { + t.eventCh <- tx + }() + } + return nil +} + +func (t *transactionRepository) GetAll(ctx context.Context) ([]domain.Transaction, error) { + var txs []domain.Transaction + err := t.db.Find(&txs, nil) + return txs, err +} + +func (t *transactionRepository) GetEventChannel() chan domain.Transaction { + return t.eventCh +} + +func (t *transactionRepository) Stop() { + close(t.eventCh) +} diff --git a/pkg/client-sdk/store/badger/utils.go b/pkg/client-sdk/store/badger/utils.go new file mode 100644 index 000000000..b590f7373 --- /dev/null +++ b/pkg/client-sdk/store/badger/utils.go @@ -0,0 +1,47 @@ +package badgerstore + +import ( + "time" + + "github.com/dgraph-io/badger/v4" + "github.com/dgraph-io/badger/v4/options" + "github.com/timshannon/badgerhold/v4" +) + +func CreateDB(dbDir string, logger badger.Logger) (*badgerhold.Store, error) { + isInMemory := len(dbDir) <= 0 + + opts := badger.DefaultOptions(dbDir) + opts.Logger = logger + + if isInMemory { + opts.InMemory = true + } else { + opts.Compression = options.ZSTD + } + + db, err := badgerhold.Open(badgerhold.Options{ + Encoder: badgerhold.DefaultEncode, + Decoder: badgerhold.DefaultDecode, + SequenceBandwith: 100, + Options: opts, + }) + if err != nil { + return nil, err + } + + if !isInMemory { + ticker := time.NewTicker(30 * time.Minute) + + go func() { + for { + <-ticker.C + if err := db.Badger().RunValueLogGC(0.5); err != nil && err != badger.ErrNoRewrite { + logger.Errorf("%s", err) + } + } + }() + } + + return db, nil +} diff --git a/pkg/client-sdk/store/badger/vtxo_repository.go b/pkg/client-sdk/store/badger/vtxo_repository.go new file mode 100644 index 000000000..982be9d7d --- /dev/null +++ b/pkg/client-sdk/store/badger/vtxo_repository.go @@ -0,0 +1,57 @@ +package badgerstore + +import ( + "context" + "fmt" + "path/filepath" + + "github.com/ark-network/ark/pkg/client-sdk/store/domain" + "github.com/dgraph-io/badger/v4" + "github.com/timshannon/badgerhold/v4" +) + +const ( + vtxoStoreDir = "vtxos" +) + +type vtxoRepository struct { + db *badgerhold.Store +} + +func NewVtxoRepository(dir string, logger badger.Logger) (domain.VtxoRepository, error) { + badgerDb, err := CreateDB(filepath.Join(dir, vtxoStoreDir), logger) + if err != nil { + return nil, fmt.Errorf("failed to open round events store: %s", err) + } + return &vtxoRepository{ + db: badgerDb, + }, nil +} + +func (v *vtxoRepository) InsertVtxos(ctx context.Context, vtxos []domain.Vtxo) error { + for _, vtxo := range vtxos { + if err := v.db.Insert(vtxo.Key(), &vtxo); err != nil { + return err + } + } + return nil +} + +func (v *vtxoRepository) GetAll( + ctx context.Context, +) (spendable []domain.Vtxo, spent []domain.Vtxo, err error) { + var allVtxos []domain.Vtxo + err = v.db.Find(&allVtxos, nil) + if err != nil { + return nil, nil, err + } + + for _, vtxo := range allVtxos { + if vtxo.Spent { + spent = append(spent, vtxo) + } else { + spendable = append(spendable, vtxo) + } + } + return +} diff --git a/pkg/client-sdk/store/domain.go b/pkg/client-sdk/store/domain/domain.go similarity index 90% rename from pkg/client-sdk/store/domain.go rename to pkg/client-sdk/store/domain/domain.go index 946400532..3cc3c213a 100644 --- a/pkg/client-sdk/store/domain.go +++ b/pkg/client-sdk/store/domain/domain.go @@ -1,13 +1,14 @@ -package store +package domain import ( + "strconv" "time" "github.com/ark-network/ark/common" "github.com/decred/dcrd/dcrec/secp256k1/v4" ) -type StoreData struct { +type ConfigData struct { AspUrl string AspPubkey *secp256k1.PublicKey WalletType string @@ -34,6 +35,10 @@ type Vtxo struct { Spent bool } +func (v Vtxo) Key() string { + return v.Txid + ":" + strconv.Itoa(int(v.VOut)) +} + const ( TxSent TxType = "sent" TxReceived TxType = "received" diff --git a/pkg/client-sdk/store/store.go b/pkg/client-sdk/store/domain/repository.go similarity index 65% rename from pkg/client-sdk/store/store.go rename to pkg/client-sdk/store/domain/repository.go index d6a7dc980..f140657b4 100644 --- a/pkg/client-sdk/store/store.go +++ b/pkg/client-sdk/store/domain/repository.go @@ -1,29 +1,27 @@ -package store +package domain -import ( - "context" -) +import "context" -const ( - InMemoryStore = "inmemory" - FileStore = "file" -) - -type ConfigStore interface { - GetType() string - GetDatadir() string - AddData(ctx context.Context, data StoreData) error - GetData(ctx context.Context) (*StoreData, error) - CleanData(ctx context.Context) error +type SdkRepository interface { + AppDataRepository() AppDataRepository + ConfigRepository() ConfigRepository } -type AppDataStore interface { +type AppDataRepository interface { TransactionRepository() TransactionRepository VtxoRepository() VtxoRepository Stop() } +type ConfigRepository interface { + GetType() string + GetDatadir() string + AddData(ctx context.Context, data ConfigData) error + GetData(ctx context.Context) (*ConfigData, error) + CleanData(ctx context.Context) error +} + type TransactionRepository interface { InsertTransactions(ctx context.Context, txs []Transaction) error GetAll(ctx context.Context) ([]Transaction, error) diff --git a/pkg/client-sdk/store/file/store.go b/pkg/client-sdk/store/file/config.go similarity index 94% rename from pkg/client-sdk/store/file/store.go rename to pkg/client-sdk/store/file/config.go index 92e990fba..9973ec17f 100644 --- a/pkg/client-sdk/store/file/store.go +++ b/pkg/client-sdk/store/file/config.go @@ -12,7 +12,7 @@ import ( "strings" "github.com/ark-network/ark/pkg/client-sdk/internal/utils" - "github.com/ark-network/ark/pkg/client-sdk/store" + "github.com/ark-network/ark/pkg/client-sdk/store/domain" "github.com/decred/dcrd/dcrec/secp256k1/v4" ) @@ -20,6 +20,26 @@ const ( filename = "state.json" ) +func NewConfig(baseDir string) (domain.ConfigRepository, error) { + if len(baseDir) <= 0 { + return nil, fmt.Errorf("missing base directory") + } + + datadir := cleanAndExpandPath(baseDir) + if err := makeDirectoryIfNotExists(datadir); err != nil { + return nil, fmt.Errorf("failed to initialize datadir: %s", err) + } + filePath := filepath.Join(datadir, filename) + + fileStore := &Store{filePath} + + if _, err := fileStore.open(); err != nil { + return nil, fmt.Errorf("failed to open store: %s", err) + } + + return fileStore, nil +} + type storeData struct { AspUrl string `json:"asp_url"` AspPubkey string `json:"asp_pubkey"` @@ -38,7 +58,7 @@ func (d storeData) isEmpty() bool { return d == storeData{} } -func (d storeData) decode() store.StoreData { +func (d storeData) decode() domain.ConfigData { network := utils.NetworkFromString(d.Network) roundLifetime, _ := strconv.Atoi(d.RoundLifetime) roundInterval, _ := strconv.Atoi(d.RoundInterval) @@ -47,7 +67,7 @@ func (d storeData) decode() store.StoreData { buf, _ := hex.DecodeString(d.AspPubkey) aspPubkey, _ := secp256k1.ParsePubKey(buf) explorerURL := d.ExplorerURL - return store.StoreData{ + return domain.ConfigData{ AspUrl: d.AspUrl, AspPubkey: aspPubkey, WalletType: d.WalletType, @@ -82,34 +102,15 @@ type Store struct { filePath string } -func NewConfigStore(baseDir string) (store.ConfigStore, error) { - if len(baseDir) <= 0 { - return nil, fmt.Errorf("missing base directory") - } - datadir := cleanAndExpandPath(baseDir) - if err := makeDirectoryIfNotExists(datadir); err != nil { - return nil, fmt.Errorf("failed to initialize datadir: %s", err) - } - filePath := filepath.Join(datadir, filename) - - fileStore := &Store{filePath} - - if _, err := fileStore.open(); err != nil { - return nil, fmt.Errorf("failed to open store: %s", err) - } - - return fileStore, nil -} - func (s *Store) GetType() string { - return store.FileStore + return "file" } func (s *Store) GetDatadir() string { return filepath.Dir(s.filePath) } -func (s *Store) AddData(ctx context.Context, data store.StoreData) error { +func (s *Store) AddData(ctx context.Context, data domain.ConfigData) error { sd := &storeData{ AspUrl: data.AspUrl, AspPubkey: hex.EncodeToString(data.AspPubkey.SerializeCompressed()), @@ -130,7 +131,7 @@ func (s *Store) AddData(ctx context.Context, data store.StoreData) error { return nil } -func (s *Store) GetData(_ context.Context) (*store.StoreData, error) { +func (s *Store) GetData(_ context.Context) (*domain.ConfigData, error) { sd, err := s.open() if err != nil { return nil, err diff --git a/pkg/client-sdk/store/inmemory/config.go b/pkg/client-sdk/store/inmemory/config.go new file mode 100644 index 000000000..9cd3cd28c --- /dev/null +++ b/pkg/client-sdk/store/inmemory/config.go @@ -0,0 +1,55 @@ +package inmemorystore + +import ( + "context" + "sync" + + "github.com/ark-network/ark/pkg/client-sdk/store/domain" +) + +type configStore struct { + data *domain.ConfigData + lock *sync.RWMutex +} + +func NewConfig() (domain.ConfigRepository, error) { + lock := &sync.RWMutex{} + return &configStore{lock: lock}, nil +} + +func (s *configStore) GetType() string { + return "inmemory" +} + +func (s *configStore) GetDatadir() string { + return "" +} + +func (s *configStore) AddData( + _ context.Context, data domain.ConfigData, +) error { + s.lock.Lock() + defer s.lock.Unlock() + + s.data = &data + return nil +} + +func (s *configStore) GetData(_ context.Context) (*domain.ConfigData, error) { + s.lock.RLock() + defer s.lock.RUnlock() + + if s.data == nil { + return nil, nil + } + + return s.data, nil +} + +func (s *configStore) CleanData(_ context.Context) error { + s.lock.Lock() + defer s.lock.Unlock() + + s.data = nil + return nil +} diff --git a/pkg/client-sdk/store/inmemory/store.go b/pkg/client-sdk/store/inmemory/store.go deleted file mode 100644 index c46d8e30b..000000000 --- a/pkg/client-sdk/store/inmemory/store.go +++ /dev/null @@ -1,55 +0,0 @@ -package inmemorystore - -import ( - "context" - "sync" - - "github.com/ark-network/ark/pkg/client-sdk/store" -) - -type Store struct { - data *store.StoreData - lock *sync.RWMutex -} - -func NewConfigStore() (store.ConfigStore, error) { - lock := &sync.RWMutex{} - return &Store{lock: lock}, nil -} - -func (s *Store) GetType() string { - return store.InMemoryStore -} - -func (s *Store) GetDatadir() string { - return "" -} - -func (s *Store) AddData( - _ context.Context, data store.StoreData, -) error { - s.lock.Lock() - defer s.lock.Unlock() - - s.data = &data - return nil -} - -func (s *Store) GetData(_ context.Context) (*store.StoreData, error) { - s.lock.RLock() - defer s.lock.RUnlock() - - if s.data == nil { - return nil, nil - } - - return s.data, nil -} - -func (s *Store) CleanData(_ context.Context) error { - s.lock.Lock() - defer s.lock.Unlock() - - s.data = nil - return nil -} diff --git a/pkg/client-sdk/store/service.go b/pkg/client-sdk/store/service.go new file mode 100644 index 000000000..63c015045 --- /dev/null +++ b/pkg/client-sdk/store/service.go @@ -0,0 +1,91 @@ +package store + +import ( + badgerstore "github.com/ark-network/ark/pkg/client-sdk/store/badger" + "github.com/ark-network/ark/pkg/client-sdk/store/domain" + "github.com/ark-network/ark/pkg/client-sdk/store/file" + "github.com/ark-network/ark/pkg/client-sdk/store/inmemory" + "github.com/dgraph-io/badger/v4" +) + +const ( + InMemoryStore = "inmemory" + FileStore = "file" + Badger = "badger" +) + +type service struct { + configRepository domain.ConfigRepository + appDataRepository domain.AppDataRepository +} + +type Config struct { + ConfigStoreType string + AppDataStoreType string + + BaseDir string + BadgerLogger badger.Logger +} + +func NewService(storeConfig Config) (domain.SdkRepository, error) { + var ( + configRepository domain.ConfigRepository + appDataRepository domain.AppDataRepository + transactionRepository domain.TransactionRepository + vtxoRepository domain.VtxoRepository + err error + + dir = storeConfig.BaseDir + badgerLogger = storeConfig.BadgerLogger + ) + + switch storeConfig.ConfigStoreType { + case InMemoryStore: + configRepository, err = inmemorystore.NewConfig() + if err != nil { + return nil, err + } + case FileStore: + configRepository, err = filestore.NewConfig(dir) + if err != nil { + return nil, err + } + } + + switch storeConfig.AppDataStoreType { + case Badger: + transactionRepository, err = badgerstore.NewTransactionRepository( + dir, + badgerLogger, + ) + if err != nil { + return nil, err + } + + vtxoRepository, err = badgerstore.NewVtxoRepository( + dir, + badgerLogger, + ) + if err != nil { + return nil, err + } + + appDataRepository = badgerstore.NewAppDataRepository( + transactionRepository, + vtxoRepository, + ) + } + + return &service{ + configRepository: configRepository, + appDataRepository: appDataRepository, + }, nil +} + +func (s *service) AppDataRepository() domain.AppDataRepository { + return s.appDataRepository +} + +func (s *service) ConfigRepository() domain.ConfigRepository { + return s.configRepository +} diff --git a/pkg/client-sdk/store/store_test.go b/pkg/client-sdk/store/service_test.go similarity index 53% rename from pkg/client-sdk/store/store_test.go rename to pkg/client-sdk/store/service_test.go index 96e82fc75..f527065f7 100644 --- a/pkg/client-sdk/store/store_test.go +++ b/pkg/client-sdk/store/service_test.go @@ -3,21 +3,24 @@ package store_test import ( "context" "testing" + "time" "github.com/ark-network/ark/common" "github.com/ark-network/ark/pkg/client-sdk/client" "github.com/ark-network/ark/pkg/client-sdk/store" - filestore "github.com/ark-network/ark/pkg/client-sdk/store/file" - inmemorystore "github.com/ark-network/ark/pkg/client-sdk/store/inmemory" + "github.com/ark-network/ark/pkg/client-sdk/store/domain" + filedb "github.com/ark-network/ark/pkg/client-sdk/store/file" + inmemorydb "github.com/ark-network/ark/pkg/client-sdk/store/inmemory" "github.com/ark-network/ark/pkg/client-sdk/wallet" "github.com/btcsuite/btcd/btcec/v2" + log "github.com/sirupsen/logrus" "github.com/stretchr/testify/require" ) func TestStore(t *testing.T) { key, _ := btcec.NewPrivateKey() ctx := context.Background() - testStoreData := store.StoreData{ + testStoreData := domain.ConfigData{ AspUrl: "localhost:7070", AspPubkey: key.PubKey(), WalletType: wallet.SingleKeyWallet, @@ -46,13 +49,13 @@ func TestStore(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - var storeSvc store.ConfigStore + var storeSvc domain.ConfigRepository var err error switch tt.name { case store.InMemoryStore: - storeSvc, err = inmemorystore.NewConfigStore() + storeSvc, err = inmemorydb.NewConfig() case store.FileStore: - storeSvc, err = filestore.NewConfigStore(t.TempDir()) + storeSvc, err = filedb.NewConfig(t.TempDir()) } require.NoError(t, err) require.NotNil(t, storeSvc) @@ -90,3 +93,75 @@ func TestStore(t *testing.T) { }) } } + +func TestNewService(t *testing.T) { + ctx := context.Background() + testDir := t.TempDir() + + dbConfig := store.Config{ + ConfigStoreType: store.FileStore, + AppDataStoreType: store.Badger, + BaseDir: testDir, + } + + service, err := store.NewService(dbConfig) + require.NoError(t, err) + require.NotNil(t, service) + + go func() { + eventCh := service.AppDataRepository().TransactionRepository().GetEventChannel() + for tx := range eventCh { + log.Infof("Tx inserted: %d %v", tx.Amount, tx.Type) + } + }() + + txRepo := service.AppDataRepository().TransactionRepository() + require.NotNil(t, txRepo) + + testTxs := []domain.Transaction{ + { + ID: "tx1", + Amount: 1000, + Type: domain.TxSent, + CreatedAt: time.Now(), + }, + { + ID: "tx2", + Amount: 2000, + Type: domain.TxReceived, + CreatedAt: time.Now(), + }, + } + err = txRepo.InsertTransactions(ctx, testTxs) + require.NoError(t, err) + + retrievedTxs, err := txRepo.GetAll(ctx) + require.NoError(t, err) + require.Len(t, retrievedTxs, 2) + + vtxoRepo := service.AppDataRepository().VtxoRepository() + require.NotNil(t, vtxoRepo) + + testVtxos := []domain.Vtxo{ + { + Txid: "vtxo1", + VOut: 0, + Amount: 1000, + }, + { + Txid: "vtxo2", + VOut: 1, + Amount: 2000, + Spent: true, + }, + } + err = vtxoRepo.InsertVtxos(ctx, testVtxos) + require.NoError(t, err) + + spendable, spent, err := vtxoRepo.GetAll(ctx) + require.NoError(t, err) + require.Len(t, spendable, 1) + require.Len(t, spent, 1) + + service.AppDataRepository().Stop() +} diff --git a/pkg/client-sdk/store/sqlite/app_data_store.go b/pkg/client-sdk/store/sqlite/app_data_store.go deleted file mode 100644 index 7b90c812c..000000000 --- a/pkg/client-sdk/store/sqlite/app_data_store.go +++ /dev/null @@ -1,96 +0,0 @@ -package sqlitestore - -import ( - "database/sql" - "errors" - "fmt" - "os" - "path/filepath" - - "github.com/ark-network/ark/pkg/client-sdk/store" - "github.com/golang-migrate/migrate/v4" - sqlitemigrate "github.com/golang-migrate/migrate/v4/database/sqlite" - _ "github.com/golang-migrate/migrate/v4/source/file" - log "github.com/sirupsen/logrus" -) - -const ( - sqliteDbFile = "appdata.sqlite.db" - driverName = "sqlite" -) - -type appDataRepository struct { - db *sql.DB - - transactionRepo store.TransactionRepository - vtxoRepo store.VtxoRepository -} - -func NewAppDataRepository( - baseDir string, migrationPath string, -) (store.AppDataStore, error) { - dbFile := filepath.Join(baseDir, sqliteDbFile) - db, err := openDb(dbFile) - if err != nil { - return nil, fmt.Errorf("failed to open db: %s", err) - } - - driver, err := sqlitemigrate.WithInstance(db, &sqlitemigrate.Config{}) - if err != nil { - return nil, err - } - - m, err := migrate.NewWithDatabaseInstance( - migrationPath, - "ark-sdk.db", - driver, - ) - if err != nil { - return nil, fmt.Errorf("failed to create migration instance: %s", err) - } - - if err := m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) { - return nil, fmt.Errorf("failed to run migrations: %s", err) - } - - return &appDataRepository{ - db: db, - transactionRepo: NewTransactionRepository(db), - vtxoRepo: NewVtxoRepository(db), - }, nil -} - -func (a *appDataRepository) TransactionRepository() store.TransactionRepository { - return a.transactionRepo -} - -func (a *appDataRepository) VtxoRepository() store.VtxoRepository { - return a.vtxoRepo -} - -func (a *appDataRepository) Stop() { - a.transactionRepo.Stop() - - if err := a.db.Close(); err != nil { - log.Warnf("failed to close app data store: %v", err) - } -} - -func openDb(dbPath string) (*sql.DB, error) { - dir := filepath.Dir(dbPath) - if _, err := os.Stat(dir); os.IsNotExist(err) { - err = os.MkdirAll(dir, 0755) - if err != nil { - return nil, fmt.Errorf("failed to create directory: %v", err) - } - } - - db, err := sql.Open(driverName, dbPath) - if err != nil { - return nil, fmt.Errorf("failed to open db: %w", err) - } - - db.SetMaxOpenConns(1) - - return db, nil -} diff --git a/pkg/client-sdk/store/sqlite/migration/20240918074848_add_transaction_table.down.sql b/pkg/client-sdk/store/sqlite/migration/20240918074848_add_transaction_table.down.sql deleted file mode 100644 index 5dee431bb..000000000 --- a/pkg/client-sdk/store/sqlite/migration/20240918074848_add_transaction_table.down.sql +++ /dev/null @@ -1,3 +0,0 @@ -DROP TABLE IF EXISTS txs; - -DROP TABLE IF EXISTS vtxo; diff --git a/pkg/client-sdk/store/sqlite/migration/20240918074848_add_transaction_table.up.sql b/pkg/client-sdk/store/sqlite/migration/20240918074848_add_transaction_table.up.sql deleted file mode 100644 index 9dadd1337..000000000 --- a/pkg/client-sdk/store/sqlite/migration/20240918074848_add_transaction_table.up.sql +++ /dev/null @@ -1,25 +0,0 @@ -CREATE TABLE IF NOT EXISTS txs ( - id TEXT PRIMARY KEY, - boarding_txid TEXT NOT NULL, - round_txid TEXT NOT NULL, - redeem_txid TEXT NOT NULL, - amount INTEGER NOT NULL, - type TEXT NOT NULL, - pending BOOLEAN NOT NULL, - claimed BOOLEAN NOT NULL, - created_at INTEGER NOT NULL -); - -CREATE TABLE vtxo ( - txid TEXT NOT NULL, - vout INTEGER NOT NULL, - amount INTEGER NOT NULL, - round_txid TEXT, - expires_at TIMESTAMP, - redeem_tx TEXT, - unconditional_forfeit_txs TEXT, - pending BOOLEAN, - spent_by TEXT, - spent BOOLEAN, - PRIMARY KEY (txid, vout) -); \ No newline at end of file diff --git a/pkg/client-sdk/store/sqlite/sqlc.yaml b/pkg/client-sdk/store/sqlite/sqlc.yaml deleted file mode 100644 index 9b06e60f2..000000000 --- a/pkg/client-sdk/store/sqlite/sqlc.yaml +++ /dev/null @@ -1,12 +0,0 @@ -version: "2" -sql: - - engine: "sqlite" - queries: "sqlc/query.sql" - schema: "migration" - gen: - go: - package: "queries" - out: "sqlc/queries" - overrides: - - go_type: "int64" - column: "vtxo.expires_at" diff --git a/pkg/client-sdk/store/sqlite/sqlc/queries/db.go b/pkg/client-sdk/store/sqlite/sqlc/queries/db.go deleted file mode 100644 index fa7857332..000000000 --- a/pkg/client-sdk/store/sqlite/sqlc/queries/db.go +++ /dev/null @@ -1,31 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.26.0 - -package queries - -import ( - "context" - "database/sql" -) - -type DBTX interface { - ExecContext(context.Context, string, ...interface{}) (sql.Result, error) - PrepareContext(context.Context, string) (*sql.Stmt, error) - QueryContext(context.Context, string, ...interface{}) (*sql.Rows, error) - QueryRowContext(context.Context, string, ...interface{}) *sql.Row -} - -func New(db DBTX) *Queries { - return &Queries{db: db} -} - -type Queries struct { - db DBTX -} - -func (q *Queries) WithTx(tx *sql.Tx) *Queries { - return &Queries{ - db: tx, - } -} diff --git a/pkg/client-sdk/store/sqlite/sqlc/queries/models.go b/pkg/client-sdk/store/sqlite/sqlc/queries/models.go deleted file mode 100644 index 0341840f1..000000000 --- a/pkg/client-sdk/store/sqlite/sqlc/queries/models.go +++ /dev/null @@ -1,34 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.26.0 - -package queries - -import ( - "database/sql" -) - -type Tx struct { - ID string - BoardingTxid string - RoundTxid string - RedeemTxid string - Amount int64 - Type string - Pending bool - Claimed bool - CreatedAt int64 -} - -type Vtxo struct { - Txid string - Vout int64 - Amount int64 - RoundTxid sql.NullString - ExpiresAt int64 - RedeemTx sql.NullString - UnconditionalForfeitTxs sql.NullString - Pending sql.NullBool - SpentBy sql.NullString - Spent sql.NullBool -} diff --git a/pkg/client-sdk/store/sqlite/sqlc/queries/query.sql.go b/pkg/client-sdk/store/sqlite/sqlc/queries/query.sql.go deleted file mode 100644 index 0d2aee8de..000000000 --- a/pkg/client-sdk/store/sqlite/sqlc/queries/query.sql.go +++ /dev/null @@ -1,125 +0,0 @@ -// Code generated by sqlc. DO NOT EDIT. -// versions: -// sqlc v1.26.0 -// source: query.sql - -package queries - -import ( - "context" -) - -const selectAllTransactions = `-- name: SelectAllTransactions :many -SELECT id, boarding_txid, round_txid, redeem_txid, amount, type, pending, claimed, created_at FROM txs -` - -// Transaction -func (q *Queries) SelectAllTransactions(ctx context.Context) ([]Tx, error) { - rows, err := q.db.QueryContext(ctx, selectAllTransactions) - if err != nil { - return nil, err - } - defer rows.Close() - var items []Tx - for rows.Next() { - var i Tx - if err := rows.Scan( - &i.ID, - &i.BoardingTxid, - &i.RoundTxid, - &i.RedeemTxid, - &i.Amount, - &i.Type, - &i.Pending, - &i.Claimed, - &i.CreatedAt, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Close(); err != nil { - return nil, err - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const selectAllVtxos = `-- name: SelectAllVtxos :many - -SELECT txid, vout, amount, round_txid, expires_at, redeem_tx, unconditional_forfeit_txs, pending, spent_by, spent FROM vtxo -` - -// Vtxo -func (q *Queries) SelectAllVtxos(ctx context.Context) ([]Vtxo, error) { - rows, err := q.db.QueryContext(ctx, selectAllVtxos) - if err != nil { - return nil, err - } - defer rows.Close() - var items []Vtxo - for rows.Next() { - var i Vtxo - if err := rows.Scan( - &i.Txid, - &i.Vout, - &i.Amount, - &i.RoundTxid, - &i.ExpiresAt, - &i.RedeemTx, - &i.UnconditionalForfeitTxs, - &i.Pending, - &i.SpentBy, - &i.Spent, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Close(); err != nil { - return nil, err - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} - -const selectBoardingTransaction = `-- name: SelectBoardingTransaction :many -SELECT id, boarding_txid, round_txid, redeem_txid, amount, type, pending, claimed, created_at FROM txs WHERE boarding_txid <> '' -` - -func (q *Queries) SelectBoardingTransaction(ctx context.Context) ([]Tx, error) { - rows, err := q.db.QueryContext(ctx, selectBoardingTransaction) - if err != nil { - return nil, err - } - defer rows.Close() - var items []Tx - for rows.Next() { - var i Tx - if err := rows.Scan( - &i.ID, - &i.BoardingTxid, - &i.RoundTxid, - &i.RedeemTxid, - &i.Amount, - &i.Type, - &i.Pending, - &i.Claimed, - &i.CreatedAt, - ); err != nil { - return nil, err - } - items = append(items, i) - } - if err := rows.Close(); err != nil { - return nil, err - } - if err := rows.Err(); err != nil { - return nil, err - } - return items, nil -} diff --git a/pkg/client-sdk/store/sqlite/sqlc/query.sql b/pkg/client-sdk/store/sqlite/sqlc/query.sql deleted file mode 100644 index 54bb19af3..000000000 --- a/pkg/client-sdk/store/sqlite/sqlc/query.sql +++ /dev/null @@ -1,12 +0,0 @@ -/* Transaction */ --- name: SelectAllTransactions :many -SELECT * FROM txs; - --- name: SelectBoardingTransaction :many -SELECT * FROM txs WHERE boarding_txid <> ''; - - -/* Vtxo */ - --- name: SelectAllVtxos :many -SELECT * FROM vtxo; \ No newline at end of file diff --git a/pkg/client-sdk/store/sqlite/transaction_repository.go b/pkg/client-sdk/store/sqlite/transaction_repository.go deleted file mode 100644 index f5114e3c9..000000000 --- a/pkg/client-sdk/store/sqlite/transaction_repository.go +++ /dev/null @@ -1,138 +0,0 @@ -package sqlitestore - -import ( - "context" - "database/sql" - "time" - - "github.com/ark-network/ark/pkg/client-sdk/store" - "github.com/ark-network/ark/pkg/client-sdk/store/sqlite/sqlc/queries" - "github.com/google/uuid" -) - -type transactionRepository struct { - db *sql.DB - querier *queries.Queries - - eventChannel chan store.Transaction -} - -func NewTransactionRepository(db *sql.DB) store.TransactionRepository { - return &transactionRepository{ - db: db, - querier: queries.New(db), - eventChannel: make(chan store.Transaction), - } -} - -const insertTransaction = ` -INSERT INTO txs ( - id, - boarding_txid, - round_txid, - redeem_txid, - amount, - type, - pending, - claimed, - created_at -) VALUES ( - ?,?, ?, ?, ?, ?, ?, ?, ? -)` - -func (t *transactionRepository) InsertTransactions(ctx context.Context, txs []store.Transaction) error { - dbTx, err := t.db.BeginTx(ctx, nil) - if err != nil { - return err - } - defer dbTx.Rollback() - - // Prepare the statement - stmt, err := dbTx.PrepareContext(ctx, insertTransaction) - if err != nil { - return err - } - defer stmt.Close() - - for _, tx := range txs { - _, err := stmt.ExecContext(ctx, - uuid.New().String(), - tx.BoardingTxid, - tx.RoundTxid, - tx.RedeemTxid, - int64(tx.Amount), - string(tx.Type), - tx.IsPending, - tx.CreatedAt.Unix(), - ) - if err != nil { - return err - } - - go func(transaction store.Transaction) { - t.eventChannel <- transaction - }(tx) - } - - if err := dbTx.Commit(); err != nil { - return err - } - - return nil -} - -func (t *transactionRepository) GetAll( - ctx context.Context, -) ([]store.Transaction, error) { - rows, err := t.querier.SelectAllTransactions(ctx) - if err != nil { - return nil, err - } - - resp := make([]store.Transaction, 0, len(rows)) - for _, row := range rows { - resp = append(resp, store.Transaction{ - ID: row.ID, - BoardingTxid: row.BoardingTxid, - RoundTxid: row.RoundTxid, - RedeemTxid: row.RedeemTxid, - Amount: uint64(row.Amount), - Type: store.TxType(row.Type), - IsPending: row.Pending, - CreatedAt: time.Unix(row.CreatedAt, 0), - }) - } - - return resp, nil -} - -func (t *transactionRepository) GetEventChannel() chan store.Transaction { - return t.eventChannel -} - -func (t *transactionRepository) Stop() { - close(t.eventChannel) -} - -func (t *transactionRepository) GetBoardingTxs(ctx context.Context) ([]store.Transaction, error) { - rows, err := t.querier.SelectBoardingTransaction(ctx) - if err != nil { - return nil, err - } - - resp := make([]store.Transaction, 0, len(rows)) - for _, row := range rows { - resp = append(resp, store.Transaction{ - ID: row.ID, - BoardingTxid: row.BoardingTxid, - RoundTxid: row.RoundTxid, - RedeemTxid: row.RedeemTxid, - Amount: uint64(row.Amount), - Type: store.TxType(row.Type), - IsPending: row.Pending, - CreatedAt: time.Unix(row.CreatedAt, 0), - }) - } - - return resp, nil -} diff --git a/pkg/client-sdk/store/sqlite/vtxo_repository.go b/pkg/client-sdk/store/sqlite/vtxo_repository.go deleted file mode 100644 index 3ee5b3fec..000000000 --- a/pkg/client-sdk/store/sqlite/vtxo_repository.go +++ /dev/null @@ -1,147 +0,0 @@ -package sqlitestore - -import ( - "context" - "database/sql" - "strings" - "time" - - "github.com/ark-network/ark/pkg/client-sdk/store" - "github.com/ark-network/ark/pkg/client-sdk/store/sqlite/sqlc/queries" -) - -type vtxoRepository struct { - db *sql.DB - querier *queries.Queries -} - -func NewVtxoRepository(db *sql.DB) store.VtxoRepository { - return &vtxoRepository{ - db: db, - querier: queries.New(db), - } -} - -func (v *vtxoRepository) GetAll( - ctx context.Context, -) (spendable []store.Vtxo, spent []store.Vtxo, err error) { - rows, err := v.querier.SelectAllVtxos(ctx) - if err != nil { - return nil, nil, err - } - - spendableVtxos := make([]store.Vtxo, 0) - spentVxos := make([]store.Vtxo, 0) - for _, v := range rows { - roundTxID := "" - if v.RoundTxid.Valid { - roundTxID = v.RoundTxid.String - } - - redeemTx := "" - if v.RedeemTx.Valid { - redeemTx = v.RedeemTx.String - } - - unconditionalForfeitTxs := make([]string, 0) - if v.UnconditionalForfeitTxs.Valid { - unconditionalForfeitTxs = strings.Split(v.UnconditionalForfeitTxs.String, ",") - } - - pending := false - if v.Pending.Valid { - pending = v.Pending.Bool - } - - spentBy := "" - if v.SpentBy.Valid { - spentBy = v.SpentBy.String - } - - spent := false - if v.Spent.Valid { - spent = v.Spent.Bool - } - - expiresAt := time.Unix(v.ExpiresAt, 0) - - vtxo := store.Vtxo{ - Txid: v.Txid, - VOut: uint32(v.Vout), - Amount: uint64(v.Amount), - RoundTxid: roundTxID, - ExpiresAt: &expiresAt, - RedeemTx: redeemTx, - UnconditionalForfeitTxs: unconditionalForfeitTxs, - Pending: pending, - SpentBy: spentBy, - Spent: spent, - } - if spent { - spentVxos = append(spentVxos, vtxo) - } else { - spendableVtxos = append(spendableVtxos, vtxo) - } - - } - - return spendableVtxos, spentVxos, nil -} - -const insertVtxos = ` -INSERT INTO vtxo ( - txid, vout, amount, round_txid, expires_at, redeem_tx, unconditional_forfeit_txs, pending, spent_by, spent - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` - -func (v *vtxoRepository) InsertVtxos(ctx context.Context, vtxos []store.Vtxo) error { - // Start a transaction - tx, err := v.db.BeginTx(ctx, nil) - if err != nil { - return err - } - defer tx.Rollback() - - // Prepare the statement - stmt, err := tx.PrepareContext(ctx, insertVtxos) - if err != nil { - return err - } - defer stmt.Close() - - for _, vtxo := range vtxos { - var expiresAt sql.NullInt64 - if vtxo.ExpiresAt != nil { - expiresAt = sql.NullInt64{Int64: vtxo.ExpiresAt.Unix(), Valid: true} - } - - unconditionalForfeitTxs := "" - if vtxo.UnconditionalForfeitTxs != nil { - for _, tx := range vtxo.UnconditionalForfeitTxs { - unconditionalForfeitTxs += tx + "," - } - } - - _, err := stmt.ExecContext(ctx, - vtxo.Txid, - vtxo.VOut, - vtxo.Amount, - vtxo.RoundTxid, - expiresAt, - vtxo.RedeemTx, - unconditionalForfeitTxs, - vtxo.Pending, - vtxo.SpentBy, - vtxo.Spent, - ) - if err != nil { - return err - } - } - - // Commit the transaction - if err := tx.Commit(); err != nil { - return err - } - - return nil -} diff --git a/pkg/client-sdk/wallet/singlekey/bitcoin_wallet.go b/pkg/client-sdk/wallet/singlekey/bitcoin_wallet.go index 825e4dc44..b6191d517 100644 --- a/pkg/client-sdk/wallet/singlekey/bitcoin_wallet.go +++ b/pkg/client-sdk/wallet/singlekey/bitcoin_wallet.go @@ -11,7 +11,7 @@ import ( "github.com/ark-network/ark/common/bitcointree" "github.com/ark-network/ark/pkg/client-sdk/explorer" "github.com/ark-network/ark/pkg/client-sdk/internal/utils" - "github.com/ark-network/ark/pkg/client-sdk/store" + "github.com/ark-network/ark/pkg/client-sdk/store/domain" "github.com/ark-network/ark/pkg/client-sdk/wallet" walletstore "github.com/ark-network/ark/pkg/client-sdk/wallet/singlekey/store" "github.com/btcsuite/btcd/btcec/v2/schnorr" @@ -26,14 +26,14 @@ type bitcoinWallet struct { } func NewBitcoinWallet( - configStore store.ConfigStore, walletStore walletstore.WalletStore, + configRepository domain.ConfigRepository, walletStore walletstore.WalletStore, ) (wallet.WalletService, error) { walletData, err := walletStore.GetWallet() if err != nil { return nil, err } return &bitcoinWallet{ - &singlekeyWallet{configStore, walletStore, nil, walletData}, + &singlekeyWallet{configRepository, walletStore, nil, walletData}, }, nil } @@ -206,7 +206,7 @@ func (w *bitcoinWallet) getAddress( return "", "", "", fmt.Errorf("wallet not initialized") } - data, err := w.configStore.GetData(ctx) + data, err := w.configRepository.GetData(ctx) if err != nil { return "", "", "", err } diff --git a/pkg/client-sdk/wallet/singlekey/liquid_wallet.go b/pkg/client-sdk/wallet/singlekey/liquid_wallet.go index 4153e498f..58c392773 100644 --- a/pkg/client-sdk/wallet/singlekey/liquid_wallet.go +++ b/pkg/client-sdk/wallet/singlekey/liquid_wallet.go @@ -11,7 +11,7 @@ import ( "github.com/ark-network/ark/common/tree" "github.com/ark-network/ark/pkg/client-sdk/explorer" "github.com/ark-network/ark/pkg/client-sdk/internal/utils" - "github.com/ark-network/ark/pkg/client-sdk/store" + "github.com/ark-network/ark/pkg/client-sdk/store/domain" "github.com/ark-network/ark/pkg/client-sdk/wallet" walletstore "github.com/ark-network/ark/pkg/client-sdk/wallet/singlekey/store" "github.com/btcsuite/btcd/btcec/v2/schnorr" @@ -27,14 +27,14 @@ type liquidWallet struct { } func NewLiquidWallet( - configStore store.ConfigStore, walletStore walletstore.WalletStore, + configRepository domain.ConfigRepository, walletStore walletstore.WalletStore, ) (wallet.WalletService, error) { walletData, err := walletStore.GetWallet() if err != nil { return nil, err } return &liquidWallet{ - &singlekeyWallet{configStore, walletStore, nil, walletData}, + &singlekeyWallet{configRepository, walletStore, nil, walletData}, }, nil } @@ -125,7 +125,7 @@ func (s *liquidWallet) SignTransaction( return "", err } - storeData, err := s.configStore.GetData(ctx) + storeData, err := s.configRepository.GetData(ctx) if err != nil { return "", err } @@ -228,7 +228,7 @@ func (w *liquidWallet) getAddress( return "", "", "", fmt.Errorf("wallet not initialized") } - data, err := w.configStore.GetData(ctx) + data, err := w.configRepository.GetData(ctx) if err != nil { return "", "", "", err } diff --git a/pkg/client-sdk/wallet/singlekey/wallet.go b/pkg/client-sdk/wallet/singlekey/wallet.go index b959d43a5..d102ca9fa 100644 --- a/pkg/client-sdk/wallet/singlekey/wallet.go +++ b/pkg/client-sdk/wallet/singlekey/wallet.go @@ -7,17 +7,17 @@ import ( "fmt" "github.com/ark-network/ark/pkg/client-sdk/internal/utils" - "github.com/ark-network/ark/pkg/client-sdk/store" + "github.com/ark-network/ark/pkg/client-sdk/store/domain" "github.com/ark-network/ark/pkg/client-sdk/wallet" walletstore "github.com/ark-network/ark/pkg/client-sdk/wallet/singlekey/store" "github.com/decred/dcrd/dcrec/secp256k1/v4" ) type singlekeyWallet struct { - configStore store.ConfigStore - walletStore walletstore.WalletStore - privateKey *secp256k1.PrivateKey - walletData *walletstore.WalletData + configRepository domain.ConfigRepository + walletStore walletstore.WalletStore + privateKey *secp256k1.PrivateKey + walletData *walletstore.WalletData } func (w *singlekeyWallet) GetType() string { diff --git a/pkg/client-sdk/wallet/wallet_test.go b/pkg/client-sdk/wallet/wallet_test.go index 7c7382ce1..cca123cb1 100644 --- a/pkg/client-sdk/wallet/wallet_test.go +++ b/pkg/client-sdk/wallet/wallet_test.go @@ -7,8 +7,8 @@ import ( "github.com/ark-network/ark/common" "github.com/ark-network/ark/pkg/client-sdk/client" - "github.com/ark-network/ark/pkg/client-sdk/store" - inmemorystore "github.com/ark-network/ark/pkg/client-sdk/store/inmemory" + "github.com/ark-network/ark/pkg/client-sdk/store/domain" + "github.com/ark-network/ark/pkg/client-sdk/store/inmemory" "github.com/ark-network/ark/pkg/client-sdk/wallet" singlekeywallet "github.com/ark-network/ark/pkg/client-sdk/wallet/singlekey" inmemorywalletstore "github.com/ark-network/ark/pkg/client-sdk/wallet/singlekey/store/inmemory" @@ -20,7 +20,7 @@ func TestWallet(t *testing.T) { ctx := context.Background() key, _ := btcec.NewPrivateKey() password := "password" - testStoreData := store.StoreData{ + testStoreData := domain.ConfigData{ AspUrl: "localhost:7070", AspPubkey: key.PubKey(), WalletType: wallet.SingleKeyWallet, @@ -54,7 +54,7 @@ func TestWallet(t *testing.T) { t.Run(tt.name, func(t *testing.T) { t.Parallel() - store, err := inmemorystore.NewConfigStore() + store, err := inmemorystore.NewConfig() require.NoError(t, err) require.NotNil(t, store) diff --git a/pkg/client-sdk/wasm/browser/config_store.go b/pkg/client-sdk/wasm/browser/config_store.go index 7b08fb38b..02dfb6169 100644 --- a/pkg/client-sdk/wasm/browser/config_store.go +++ b/pkg/client-sdk/wasm/browser/config_store.go @@ -37,7 +37,7 @@ type localStorageStore struct { store js.Value } -func NewLocalStorageStore() (store.ConfigStore, error) { +func NewLocalStorageStore() (db.ConfigStore, error) { store := js.Global().Get("localStorage") return &localStorageStore{store}, nil } @@ -50,7 +50,7 @@ func (s *localStorageStore) GetDatadir() string { return "" } -func (s *localStorageStore) AddData(ctx context.Context, data store.StoreData) error { +func (s *localStorageStore) AddData(ctx context.Context, data db.ConfigData) error { sd := &storeData{ AspUrl: data.AspUrl, AspPubkey: hex.EncodeToString(data.AspPubkey.SerializeCompressed()), @@ -65,7 +65,7 @@ func (s *localStorageStore) AddData(ctx context.Context, data store.StoreData) e return s.writeData(sd) } -func (s *localStorageStore) GetData(ctx context.Context) (*store.StoreData, error) { +func (s *localStorageStore) GetData(ctx context.Context) (*db.ConfigData, error) { key := s.store.Call("getItem", "asp_pubkey") if key.IsNull() || key.IsUndefined() { return nil, nil @@ -88,7 +88,7 @@ func (s *localStorageStore) GetData(ctx context.Context) (*store.StoreData, erro unilateralExitDelay, _ := strconv.Atoi(s.store.Call("getItem", "unilateral_exit_delay").String()) dust, _ := strconv.Atoi(s.store.Call("getItem", "min_relay_fee").String()) - return &store.StoreData{ + return &db.ConfigData{ AspUrl: s.store.Call("getItem", "asp_url").String(), AspPubkey: aspPubkey, WalletType: s.store.Call("getItem", "wallet_type").String(), diff --git a/pkg/client-sdk/wasm/browser/exports.go b/pkg/client-sdk/wasm/browser/exports.go index 25ee785dd..60ea726e8 100644 --- a/pkg/client-sdk/wasm/browser/exports.go +++ b/pkg/client-sdk/wasm/browser/exports.go @@ -18,7 +18,7 @@ import ( var ( arkSdkClient arksdk.ArkClient - configStore store.ConfigStore + configStore db.ConfigStore ) func init() { @@ -45,7 +45,7 @@ func init() { } func NewCovenantClient( - ctx context.Context, storeSvc store.ConfigStore, + ctx context.Context, storeSvc db.ConfigStore, ) error { var err error @@ -80,7 +80,7 @@ func NewCovenantClient( } func NewCovenantlessClient( - ctx context.Context, storeSvc store.ConfigStore, + ctx context.Context, storeSvc db.ConfigStore, ) error { var err error @@ -123,7 +123,7 @@ func getWalletStore(storeType string) (walletstore.WalletStore, error) { } func getSingleKeyWallet( - configStore store.ConfigStore, network string, + configStore db.ConfigStore, network string, ) (wallet.WalletService, error) { walletStore, err := getWalletStore(configStore.GetType()) if err != nil { From e78f5bae2bba5fe624078a66f6904e221816b9a6 Mon Sep 17 00:00:00 2001 From: sekulicd Date: Mon, 23 Sep 2024 16:34:35 +0200 Subject: [PATCH 08/15] fix --- server/test/e2e/covenant/e2e_test.go | 10 +++++++--- server/test/e2e/covenantless/e2e_test.go | 10 +++++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/server/test/e2e/covenant/e2e_test.go b/server/test/e2e/covenant/e2e_test.go index d509fe276..c3034b19e 100644 --- a/server/test/e2e/covenant/e2e_test.go +++ b/server/test/e2e/covenant/e2e_test.go @@ -16,7 +16,7 @@ import ( grpcclient "github.com/ark-network/ark/pkg/client-sdk/client/grpc" "github.com/ark-network/ark/pkg/client-sdk/explorer" "github.com/ark-network/ark/pkg/client-sdk/redemption" - inmemorystore "github.com/ark-network/ark/pkg/client-sdk/store/inmemory" + "github.com/ark-network/ark/pkg/client-sdk/store" utils "github.com/ark-network/ark/server/test/e2e" "github.com/stretchr/testify/require" ) @@ -298,10 +298,14 @@ func setupAspWallet() error { } func setupArkSDK(t *testing.T) (arksdk.ArkClient, client.ASPClient) { - storeSvc, err := inmemorystore.NewConfigStore() + appDataStore, err := store.NewService(store.Config{ + ConfigStoreType: store.FileStore, + AppDataStoreType: store.Badger, + BaseDir: t.TempDir(), + }) require.NoError(t, err) - client, err := arksdk.NewCovenantClient(storeSvc) + client, err := arksdk.NewCovenantClient(appDataStore) require.NoError(t, err) err = client.Init(context.Background(), arksdk.InitArgs{ diff --git a/server/test/e2e/covenantless/e2e_test.go b/server/test/e2e/covenantless/e2e_test.go index 18bb1161c..277d4e439 100644 --- a/server/test/e2e/covenantless/e2e_test.go +++ b/server/test/e2e/covenantless/e2e_test.go @@ -16,7 +16,7 @@ import ( grpcclient "github.com/ark-network/ark/pkg/client-sdk/client/grpc" "github.com/ark-network/ark/pkg/client-sdk/explorer" "github.com/ark-network/ark/pkg/client-sdk/redemption" - inmemorystore "github.com/ark-network/ark/pkg/client-sdk/store/inmemory" + "github.com/ark-network/ark/pkg/client-sdk/store" utils "github.com/ark-network/ark/server/test/e2e" "github.com/stretchr/testify/require" ) @@ -450,10 +450,14 @@ func setupAspWallet() error { } func setupArkSDK(t *testing.T) (arksdk.ArkClient, client.ASPClient) { - storeSvc, err := inmemorystore.NewConfigStore() + appDataStore, err := store.NewService(store.Config{ + ConfigStoreType: store.FileStore, + AppDataStoreType: store.Badger, + BaseDir: t.TempDir(), + }) require.NoError(t, err) - client, err := arksdk.NewCovenantlessClient(storeSvc) + client, err := arksdk.NewCovenantlessClient(appDataStore) require.NoError(t, err) err = client.Init(context.Background(), arksdk.InitArgs{ From ad2f8888776bc6e3d2635f86abe1a86bd9afad03 Mon Sep 17 00:00:00 2001 From: sekulicd Date: Mon, 23 Sep 2024 16:37:23 +0200 Subject: [PATCH 09/15] go sync --- pkg/client-sdk/go.mod | 15 ++++++++++++++- pkg/client-sdk/go.sum | 14 ++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/pkg/client-sdk/go.mod b/pkg/client-sdk/go.mod index 0cfe30cd8..5d925f82e 100644 --- a/pkg/client-sdk/go.mod +++ b/pkg/client-sdk/go.mod @@ -11,14 +11,17 @@ require ( github.com/btcsuite/btcd/btcutil/psbt v1.1.9 github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 + github.com/dgraph-io/badger/v4 v4.2.0 github.com/go-openapi/errors v0.22.0 github.com/go-openapi/runtime v0.28.0 github.com/go-openapi/strfmt v0.23.0 github.com/go-openapi/swag v0.23.0 github.com/go-openapi/validate v0.24.0 + github.com/google/uuid v1.6.0 github.com/lightningnetwork/lnd v0.18.2-beta github.com/sirupsen/logrus v1.9.3 github.com/stretchr/testify v1.9.0 + github.com/timshannon/badgerhold/v4 v4.0.3 github.com/vulpemventures/go-elements v0.5.4 golang.org/x/crypto v0.26.0 google.golang.org/grpc v1.65.0 @@ -31,9 +34,12 @@ require ( github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect github.com/btcsuite/winsvc v1.0.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect github.com/decred/dcrd/lru v1.1.3 // indirect + github.com/dgraph-io/ristretto v0.1.1 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -42,25 +48,32 @@ require ( github.com/go-openapi/jsonreference v0.21.0 // indirect github.com/go-openapi/loads v0.22.0 // indirect github.com/go-openapi/spec v0.21.0 // indirect + github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang/glog v1.2.1 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/golang/protobuf v1.5.4 // indirect github.com/golang/snappy v0.0.4 // indirect - github.com/google/uuid v1.6.0 // indirect + github.com/google/flatbuffers v24.3.25+incompatible // indirect github.com/gorilla/websocket v1.5.3 // indirect github.com/jessevdk/go-flags v1.6.1 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/jrick/logrotate v1.0.0 // indirect github.com/kkdai/bstream v1.0.0 // indirect + github.com/klauspost/compress v1.17.9 // indirect github.com/lightningnetwork/lnd/fn v1.2.1 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/oklog/ulid v1.3.1 // indirect github.com/onsi/ginkgo v1.16.4 // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect + github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect github.com/vulpemventures/fastsha256 v0.0.0-20160815193821-637e65642941 // indirect go.mongodb.org/mongo-driver v1.14.0 // indirect + go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/otel v1.30.0 // indirect go.opentelemetry.io/otel/metric v1.30.0 // indirect go.opentelemetry.io/otel/sdk v1.28.0 // indirect diff --git a/pkg/client-sdk/go.sum b/pkg/client-sdk/go.sum index 6a6d428c4..ebcc7ea43 100644 --- a/pkg/client-sdk/go.sum +++ b/pkg/client-sdk/go.sum @@ -31,6 +31,7 @@ github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3 github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= github.com/btcsuite/winsvc v1.0.0 h1:J9B4L7e3oqhXOcm+2IuNApwzQec85lE+QaikUcCs+dk= github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/davecgh/go-spew v0.0.0-20171005155431-ecdeabc65495/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -45,6 +46,10 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3 github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= github.com/decred/dcrd/lru v1.1.3 h1:w9EAbvGLyzm6jTjF83UKuqZEiUtJmvRhQDOCEIvSuE0= github.com/decred/dcrd/lru v1.1.3/go.mod h1:Tw0i0pJyiLEx/oZdHLe1Wdv/Y7EGzAX+sYftnmxBR4o= +github.com/dgraph-io/badger/v4 v4.2.0 h1:kJrlajbXXL9DFTNuhhu9yCx7JJa4qpYWxtE8BzuWsEs= +github.com/dgraph-io/ristretto v0.1.1 h1:6CWw5tJNgpegArSHpNHJKldNeq03FQCwYvfMVWajOK8= +github.com/dgryski/go-farm v0.0.0-20190423205320-6a90982ecee2 h1:tdlZCpZ/P9DhczCTSixgIKmwPv6+wP5DGjqLYw5SUiA= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= @@ -73,6 +78,9 @@ github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+Gr github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-openapi/validate v0.24.0 h1:LdfDKwNbpB6Vn40xhTdNZAnfLECL81w+VX3BumrGD58= github.com/go-openapi/validate v0.24.0/go.mod h1:iyeX1sEufmv3nPbBdX3ieNviWnOZaJ1+zquzJEf2BAQ= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/golang/glog v1.2.1 h1:OptwRhECazUx5ix5TTWC3EZhsZEHWcYWY4FQHTIubm4= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= @@ -80,8 +88,10 @@ github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrU github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/google/flatbuffers v24.3.25+incompatible h1:CX395cjN9Kke9mmalRoL3d81AtFUxJM+yDthflgJGkI= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -104,6 +114,7 @@ github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlT github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= github.com/kkdai/bstream v1.0.0 h1:Se5gHwgp2VT2uHfDrkbbgbgEvV9cimLELwrPJctSjg8= github.com/kkdai/bstream v1.0.0/go.mod h1:FDnDOHt5Yx4p3FaHcioFT0QjDOtgUpvjeZqAs+NVZZA= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -132,6 +143,7 @@ github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1y github.com/onsi/gomega v1.26.0 h1:03cDLK28U6hWvCAns6NeydX3zIm4SF3ci69ulidS32Q= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -151,6 +163,7 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/timshannon/badgerhold/v4 v4.0.3 h1:W6pd2qckoXw2cl8eH0ZCV/9CXNaXvaM26tzFi5Tj+v8= github.com/vulpemventures/fastsha256 v0.0.0-20160815193821-637e65642941 h1:CTcw80hz/Sw8hqlKX5ZYvBUF5gAHSHwdjXxRf/cjDcI= github.com/vulpemventures/fastsha256 v0.0.0-20160815193821-637e65642941/go.mod h1:GXBJykxW2kUcktGdsgyay7uwwWvkljASfljNcT0mbh8= github.com/vulpemventures/go-elements v0.5.4 h1:l94xoa9aYPPWiOB7Pmi08rKYvdk/n/sQIbLkQfEAASc= @@ -159,6 +172,7 @@ github.com/vulpemventures/go-secp256k1-zkp v1.1.6 h1:BmsrmXRLUibwa75Qkk8yELjpzCz github.com/vulpemventures/go-secp256k1-zkp v1.1.6/go.mod h1:zo7CpgkuPgoe7fAV+inyxsI9IhGmcoFgyD8nqZaPSOM= go.mongodb.org/mongo-driver v1.14.0 h1:P98w8egYRjYe3XDjxhYJagTokP/H6HzlsnojRgZRd80= go.mongodb.org/mongo-driver v1.14.0/go.mod h1:Vzb0Mk/pa7e6cWw85R4F/endUC3u0U9jGcNU603k65c= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opentelemetry.io/otel v1.30.0 h1:F2t8sK4qf1fAmY9ua4ohFS/K+FUuOPemHUIXHtktrts= go.opentelemetry.io/otel/metric v1.30.0 h1:4xNulvn9gjzo4hjg+wzIKG7iNFEaBMX00Qd4QIZs7+w= go.opentelemetry.io/otel/sdk v1.28.0 h1:b9d7hIry8yZsgtbmM0DKyPWMMUMlK9NEKuIG4aBqWyE= From f2288588d9a87f5d5e149a5b6dd95b0cd9652381 Mon Sep 17 00:00:00 2001 From: sekulicd Date: Tue, 24 Sep 2024 14:14:57 +0200 Subject: [PATCH 10/15] fix --- pkg/client-sdk/covenant_client.go | 25 ++++++++++--------- .../store/badger/transaction_repository.go | 6 ++--- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/pkg/client-sdk/covenant_client.go b/pkg/client-sdk/covenant_client.go index 0ec848355..e3b6f7a2c 100644 --- a/pkg/client-sdk/covenant_client.go +++ b/pkg/client-sdk/covenant_client.go @@ -621,12 +621,21 @@ func (a *covenantArkClient) listenToVtxoChan(ctnx context.Context) error { allBoardingTxs := a.getBoardingTxs(ctx) oldVtxos := append(spendableVtxosOld, spentVtxosOld...) - if len(oldVtxos) == 0 { + oldBoardingTxs, err := a.sdkRepository.AppDataRepository(). + TransactionRepository().GetBoardingTxs(ctx) + if err != nil { + log.Errorf("failed to get boarding txs: %s", err) + continue + } + + newBoardingTxs := filterNewBoardingTxs(allBoardingTxs, oldBoardingTxs) + + if len(oldVtxos) == 0 && len(newBoardingTxs) > 0 { err = a.insertInitialVtxosAndTransactions( ctx, allSpendableVtxos, allSpentVtxos, - allBoardingTxs, + newBoardingTxs, ) if err != nil { log.Errorf("failed to process new vtxos: %s", err) @@ -637,7 +646,7 @@ func (a *covenantArkClient) listenToVtxoChan(ctnx context.Context) error { allSpendableVtxos, allSpentVtxos, oldVtxos, - allBoardingTxs, + newBoardingTxs, ) if err != nil { log.Errorf("failed to update vtxos: %s", err) @@ -689,7 +698,7 @@ func (a *covenantArkClient) insertNewTxsAndTransactions( allSpendableVtxos, allSpentVtxos []client.Vtxo, oldVtxos []domain.Vtxo, - allBoardingTxs []domain.Transaction, + newBoardingTxs []domain.Transaction, ) error { spendableVtxosMap := make(map[string]client.Vtxo) spentVtxosMap := make(map[string]client.Vtxo) @@ -713,14 +722,6 @@ func (a *covenantArkClient) insertNewTxsAndTransactions( newSpendableVtxos := getNewVtxos(spendableVtxosMap, oldVtxosMap) newSpentVtxos := getNewVtxos(spentVtxosMap, oldVtxosMap) - oldBoardingTxs, err := a.sdkRepository.AppDataRepository(). - TransactionRepository().GetBoardingTxs(ctx) - if err != nil { - return err - } - - newBoardingTxs := filterNewBoardingTxs(allBoardingTxs, oldBoardingTxs) - txs, err := vtxosToTxsCovenant( a.ConfigData.RoundInterval, newSpendableVtxos, diff --git a/pkg/client-sdk/store/badger/transaction_repository.go b/pkg/client-sdk/store/badger/transaction_repository.go index 25ce9fa47..e675ba7ea 100644 --- a/pkg/client-sdk/store/badger/transaction_repository.go +++ b/pkg/client-sdk/store/badger/transaction_repository.go @@ -46,9 +46,9 @@ func (t *transactionRepository) InsertTransactions(ctx context.Context, txs []do if err := t.db.Insert(tx.ID, &tx); err != nil { return err } - go func() { - t.eventCh <- tx - }() + go func(trx domain.Transaction) { + t.eventCh <- trx + }(tx) } return nil } From 81b40d3b77d34d62b8e4b31793692937cea2bdab Mon Sep 17 00:00:00 2001 From: sekulicd Date: Wed, 25 Sep 2024 16:57:11 +0200 Subject: [PATCH 11/15] listen vtxo refactor --- pkg/client-sdk/client.go | 72 +++-- pkg/client-sdk/client_test.go | 49 ++- pkg/client-sdk/covenant_client.go | 282 ++++++++---------- pkg/client-sdk/covenant_client_test.go | 187 ++++++++++++ pkg/client-sdk/covenantless_client.go | 215 ++++++------- .../example/covenant/alice_to_bob.go | 2 +- .../store/badger/transaction_repository.go | 36 ++- .../store/badger/vtxo_repository.go | 7 + pkg/client-sdk/store/domain/domain.go | 10 +- pkg/client-sdk/store/domain/repository.go | 2 + pkg/client-sdk/store/service_test.go | 11 +- 11 files changed, 533 insertions(+), 340 deletions(-) create mode 100644 pkg/client-sdk/covenant_client_test.go diff --git a/pkg/client-sdk/client.go b/pkg/client-sdk/client.go index 7e51a1344..b29362882 100644 --- a/pkg/client-sdk/client.go +++ b/pkg/client-sdk/client.go @@ -327,37 +327,61 @@ func getCreatedAtFromExpiry(roundLifetime int64, expiry time.Time) time.Time { return expiry.Add(-time.Duration(roundLifetime) * time.Second) } -func getNewVtxos( - newVtxosMap map[string]client.Vtxo, - oldVtxosMap map[string]domain.Vtxo, -) []client.Vtxo { - newVtxos := make([]client.Vtxo, 0) - for key, vtxo := range newVtxosMap { - if _, ok := oldVtxosMap[key]; !ok { - newVtxos = append(newVtxos, vtxo) +func findNewTxs(oldTxs, newTxs []domain.Transaction) ([]domain.Transaction, error) { + newTxsMap := make(map[string]domain.Transaction) + for _, tx := range newTxs { + newTxsMap[tx.Key()] = tx + } + + oldTxsMap := make(map[string]domain.Transaction) + for _, tx := range oldTxs { + oldTxsMap[tx.Key()] = tx + } + + var result []domain.Transaction + for _, tx := range newTxs { + if _, ok := oldTxsMap[tx.Key()]; !ok { + result = append(result, tx) } } - return newVtxos + + return result, nil } -func filterNewBoardingTxs( - allBoardingTxs []domain.Transaction, - oldBoardingTxs []domain.Transaction, -) []domain.Transaction { - newBoardingTxs := make([]domain.Transaction, 0) - for _, tx := range allBoardingTxs { - found := false - for _, oldTx := range oldBoardingTxs { - if tx.BoardingTxid == oldTx.BoardingTxid { - found = true - break - } +func updateBoardingTxsState( + allBoardingTxs, oldBoardingTxs []domain.Transaction, +) ([]domain.Transaction, []domain.Transaction) { + var newBoardingTxs []domain.Transaction + var updatedOldBoardingTxs []domain.Transaction + + newTxsMap := make(map[string]bool) + for _, newTx := range allBoardingTxs { + newTxsMap[newTx.BoardingTxid] = true + } + + for _, oldTx := range oldBoardingTxs { + if !newTxsMap[oldTx.BoardingTxid] { + oldTx.IsPending = false + updatedOldBoardingTxs = append(updatedOldBoardingTxs, oldTx) + } + } + + for _, newTx := range allBoardingTxs { + if !containsTx(oldBoardingTxs, newTx.BoardingTxid) { + newBoardingTxs = append(newBoardingTxs, newTx) } - if !found { - newBoardingTxs = append(newBoardingTxs, tx) + } + + return newBoardingTxs, updatedOldBoardingTxs +} + +func containsTx(txs []domain.Transaction, txid string) bool { + for _, tx := range txs { + if tx.BoardingTxid == txid { + return true } } - return newBoardingTxs + return false } func convertVtxosToDomainVtxos(vtxos []client.Vtxo, spent bool) []domain.Vtxo { diff --git a/pkg/client-sdk/client_test.go b/pkg/client-sdk/client_test.go index 7630527f8..60e3a1f21 100644 --- a/pkg/client-sdk/client_test.go +++ b/pkg/client-sdk/client_test.go @@ -23,13 +23,15 @@ func TestVtxosToTxsCovenant(t *testing.T) { fixture: aliceToBobCovenant, want: []domain.Transaction{ { - Amount: 100000000, - Type: domain.TxReceived, + RoundTxid: "31e744a81cdd7fcc5517130a7f35722bea4dbf73faa4f4c580a6b93b2df0746d", + Amount: 20000, + Type: domain.TxSent, IsPending: false, }, { - Amount: 20000, - Type: domain.TxSent, + RoundTxid: "52dd02e90d70e2ca24f3e0d41bf6382ae98efaa99177036dc261df93a5790d7d", + Amount: 100000000, + Type: domain.TxReceived, IsPending: false, }, }, @@ -38,11 +40,11 @@ func TestVtxosToTxsCovenant(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - vtxos, boardingTxs, err := loadFixtures(tt.fixture) + vtxos, _, err := loadFixtures(tt.fixture) if err != nil { t.Fatalf("failed to load fixture: %s", err) } - got, err := vtxosToTxsCovenant(30, vtxos.spendable, vtxos.spent, boardingTxs) + got, err := vtxosToTxsCovenant(30, vtxos.spendable, vtxos.spent) require.NoError(t, err) require.Len(t, got, len(tt.want)) @@ -82,13 +84,6 @@ func TestVtxosToTxsCovenantless(t *testing.T) { name: "Alice After Sending Async", fixture: aliceAfterSendingAsync, want: []domain.Transaction{ - { - RoundTxid: "377fa2fbd27c82bdbc095478384c88b6c75432c0ef464189e49c965194446cdf", - Amount: 20000, - Type: domain.TxReceived, - IsPending: false, - CreatedAt: time.Unix(1726054898, 0), - }, { RedeemTxid: "94fa598302f17f00c8881e742ec0ce2f8c8d16f3d54fe6ba0fb7d13a493d84ad", Amount: 1000, @@ -103,18 +98,18 @@ func TestVtxosToTxsCovenantless(t *testing.T) { fixture: bobBeforeClaimingAsync, want: []domain.Transaction{ { - RedeemTxid: "766fc46ba5c2da41cd4c4bc0566e0f4e0f24c184c41acd3bead5cd7b11120367", - Amount: 2000, + RedeemTxid: "94fa598302f17f00c8881e742ec0ce2f8c8d16f3d54fe6ba0fb7d13a493d84ad", + Amount: 1000, Type: domain.TxReceived, IsPending: true, - CreatedAt: time.Unix(1726486359, 0), + CreatedAt: time.Unix(1726054898, 0), }, { - RedeemTxid: "94fa598302f17f00c8881e742ec0ce2f8c8d16f3d54fe6ba0fb7d13a493d84ad", - Amount: 1000, + RedeemTxid: "766fc46ba5c2da41cd4c4bc0566e0f4e0f24c184c41acd3bead5cd7b11120367", + Amount: 2000, Type: domain.TxReceived, IsPending: true, - CreatedAt: time.Unix(1726054898, 0), + CreatedAt: time.Unix(1726486359, 0), }, }, }, @@ -169,11 +164,11 @@ func TestVtxosToTxsCovenantless(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - vtxos, boardingTxs, err := loadFixtures(tt.fixture) + vtxos, _, err := loadFixtures(tt.fixture) if err != nil { t.Fatalf("failed to load fixture: %s", err) } - got, err := vtxosToTxsCovenantless(30, vtxos.spendable, vtxos.spent, boardingTxs) + got, err := vtxosToTxsCovenantless(30, vtxos.spendable, vtxos.spent) require.NoError(t, err) require.Len(t, got, len(tt.want)) @@ -347,19 +342,9 @@ func parseTimestamp(timestamp string) (time.Time, error) { var ( // bellow fixtures are used in bellow scenario: // 1. Alice onboards with 100000000 - // 2. Alice sends 1000 to Bob + // 2. Alice sends 20000 to Bob aliceToBobCovenant = ` { - "boardingTxs": [ - { - "boardingTxid": "69ccb6520e0b91ac1cbaa459b16ec1e3ff5f6349990b0d149dd8e6c6485d316c", - "roundTxid": "377fa2fbd27c82bdbc095478384c88b6c75432c0ef464189e49c965194446cdf", - "amount": 20000, - "pending": false, - "claimed": true, - "createdAt": "1726503865" - } - ], "spendableVtxos": [ { "outpoint": { diff --git a/pkg/client-sdk/covenant_client.go b/pkg/client-sdk/covenant_client.go index e3b6f7a2c..c43ae8910 100644 --- a/pkg/client-sdk/covenant_client.go +++ b/pkg/client-sdk/covenant_client.go @@ -6,7 +6,6 @@ import ( "encoding/hex" "fmt" "math" - "sort" "strings" "sync" "time" @@ -611,47 +610,17 @@ func (a *covenantArkClient) listenToVtxoChan(ctnx context.Context) error { continue } - spendableVtxosOld, spentVtxosOld, err := a.sdkRepository. - AppDataRepository().VtxoRepository().GetAll(ctx) - if err != nil { - log.Errorf("failed to get vtxos: %s", err) - continue - } - allBoardingTxs := a.getBoardingTxs(ctx) - oldVtxos := append(spendableVtxosOld, spentVtxosOld...) - oldBoardingTxs, err := a.sdkRepository.AppDataRepository(). - TransactionRepository().GetBoardingTxs(ctx) - if err != nil { - log.Errorf("failed to get boarding txs: %s", err) + if err := a.processVtxosAndTxs( + ctx, + allSpendableVtxos, + allSpentVtxos, + allBoardingTxs, + ); err != nil { + log.Errorf("failed to process vtxos: %s", err) continue } - - newBoardingTxs := filterNewBoardingTxs(allBoardingTxs, oldBoardingTxs) - - if len(oldVtxos) == 0 && len(newBoardingTxs) > 0 { - err = a.insertInitialVtxosAndTransactions( - ctx, - allSpendableVtxos, - allSpentVtxos, - newBoardingTxs, - ) - if err != nil { - log.Errorf("failed to process new vtxos: %s", err) - } - } else { - err := a.insertNewTxsAndTransactions( - ctx, - allSpendableVtxos, - allSpentVtxos, - oldVtxos, - newBoardingTxs, - ) - if err != nil { - log.Errorf("failed to update vtxos: %s", err) - } - } } } }(ctnx) @@ -659,92 +628,123 @@ func (a *covenantArkClient) listenToVtxoChan(ctnx context.Context) error { return nil } -func (a *covenantArkClient) insertInitialVtxosAndTransactions( +func (a *covenantArkClient) processVtxosAndTxs( ctx context.Context, - spendableVtxos, - spentVtxos []client.Vtxo, - boardingTxs []domain.Transaction, + allSpendableVtxos, + allSpentVtxos []client.Vtxo, + allBoardingTxs []domain.Transaction, +) error { + if err := a.processBoardingTxs(ctx, allBoardingTxs); err != nil { + return fmt.Errorf("failed to process txs: %s", err) + } + + return a.processVtxos(ctx, allSpendableVtxos, allSpentVtxos) +} + +func (a *covenantArkClient) processVtxos( + ctx context.Context, + allSpendableVtxos, + allSpentVtxos []client.Vtxo, ) error { - txs, err := vtxosToTxsCovenant( + spendableVtxosOld, spentVtxosOld, err := a.sdkRepository. + AppDataRepository().VtxoRepository().GetAll(ctx) + if err != nil { + return fmt.Errorf("failed to get vtxos: %s", err) + } + + oldVtxos := append(spendableVtxosOld, spentVtxosOld...) + + allTxs, err := vtxosToTxsCovenant( a.ConfigData.RoundInterval, - spendableVtxos, - spentVtxos, - boardingTxs, + allSpendableVtxos, + allSpentVtxos, ) if err != nil { return err } - - if len(txs) > 0 { - if err := a.sdkRepository.AppDataRepository().TransactionRepository(). - InsertTransactions(ctx, txs); err != nil { - return err + if len(oldVtxos) == 0 { + err = a.insertVtxosAndTransactions( + ctx, + allTxs, + allSpendableVtxos, + allSpentVtxos, + ) + if err != nil { + return fmt.Errorf("failed to process new vtxos: %s", err) + } + } else { + oldTxs, err := a.sdkRepository.AppDataRepository().TransactionRepository().GetAll(ctx) + if err != nil { + return fmt.Errorf("failed to get old transactions: %s", err) } - } - - domainVtxos := convertVtxosToDomainVtxos(spendableVtxos, false) - domainVtxos = append(domainVtxos, convertVtxosToDomainVtxos(spentVtxos, true)...) - if len(domainVtxos) > 0 { - return a.sdkRepository.AppDataRepository().VtxoRepository(). - InsertVtxos(ctx, domainVtxos) + newTxs, err := findNewTxs(oldTxs, allTxs) + err = a.insertVtxosAndTransactions( + ctx, + newTxs, + allSpendableVtxos, + allSpentVtxos, + ) + if err != nil { + return fmt.Errorf("failed to process new vtxos: %s", err) + } } return nil } -func (a *covenantArkClient) insertNewTxsAndTransactions( +func (a *covenantArkClient) processBoardingTxs( ctx context.Context, - allSpendableVtxos, - allSpentVtxos []client.Vtxo, - oldVtxos []domain.Vtxo, - newBoardingTxs []domain.Transaction, + allBoardingTxs []domain.Transaction, ) error { - spendableVtxosMap := make(map[string]client.Vtxo) - spentVtxosMap := make(map[string]client.Vtxo) - oldVtxosMap := make(map[string]domain.Vtxo) - - for _, vtxo := range allSpendableVtxos { - key := fmt.Sprintf("%s:%d", vtxo.Txid, vtxo.VOut) - spendableVtxosMap[key] = vtxo + oldBoardingTxs, err := a.sdkRepository.AppDataRepository(). + TransactionRepository().GetBoardingTxs(ctx) + if err != nil { + return fmt.Errorf("failed to get boarding txs: %s", err) } - for _, vtxo := range allSpentVtxos { - key := fmt.Sprintf("%s:%d", vtxo.Txid, vtxo.VOut) - spentVtxosMap[key] = vtxo - } + if len(oldBoardingTxs) == 0 { + if err := a.sdkRepository.AppDataRepository().TransactionRepository(). + InsertTransactions(ctx, allBoardingTxs); err != nil { + return fmt.Errorf("failed to insert boarding txs: %s", err) + } + } else { + newBoardingTxs, updatedOldBoardingTxs := updateBoardingTxsState(allBoardingTxs, oldBoardingTxs) + if err := a.sdkRepository.AppDataRepository().TransactionRepository(). + InsertTransactions(ctx, newBoardingTxs); err != nil { + return fmt.Errorf("failed to insert boarding txs: %s", err) + } - for _, vtxo := range oldVtxos { - key := fmt.Sprintf("%s:%d", vtxo.Txid, vtxo.VOut) - oldVtxosMap[key] = vtxo + if err := a.sdkRepository.AppDataRepository().TransactionRepository(). + UpdateTransactions(ctx, updatedOldBoardingTxs); err != nil { + return fmt.Errorf("failed to update boarding txs: %s", err) + } } - newSpendableVtxos := getNewVtxos(spendableVtxosMap, oldVtxosMap) - newSpentVtxos := getNewVtxos(spentVtxosMap, oldVtxosMap) + return nil +} - txs, err := vtxosToTxsCovenant( - a.ConfigData.RoundInterval, - newSpendableVtxos, - newSpentVtxos, - newBoardingTxs, - ) - if err != nil { +func (a *covenantArkClient) insertVtxosAndTransactions( + ctx context.Context, + txs []domain.Transaction, + spendableVtxos []client.Vtxo, + spentVtxos []client.Vtxo, +) error { + if err := a.sdkRepository.AppDataRepository().TransactionRepository(). + InsertTransactions(ctx, txs); err != nil { return err } - if len(txs) > 0 { - if err := a.sdkRepository.AppDataRepository().TransactionRepository(). - InsertTransactions(ctx, txs); err != nil { + domainVtxos := convertVtxosToDomainVtxos(spendableVtxos, false) + domainVtxos = append(domainVtxos, convertVtxosToDomainVtxos(spentVtxos, true)...) + + if len(domainVtxos) > 0 { + if err := a.sdkRepository.AppDataRepository().VtxoRepository().DeleteAll(ctx); err != nil { return err } - } - domainSpendableVtxos := convertVtxosToDomainVtxos(newSpendableVtxos, false) - domainSpentVtxos := convertVtxosToDomainVtxos(newSpentVtxos, true) - - if len(domainSpendableVtxos) > 0 || len(domainSpentVtxos) > 0 { return a.sdkRepository.AppDataRepository().VtxoRepository(). - InsertVtxos(ctx, append(domainSpentVtxos, domainSpendableVtxos...)) + InsertVtxos(ctx, domainVtxos) } return nil @@ -1863,10 +1863,15 @@ func (a *covenantArkClient) getBoardingTxs(ctx context.Context) (transactions [] } for _, u := range allUtxos { + pending := false + if isPending[u.Txid] { + pending = true + } transactions = append(transactions, domain.Transaction{ BoardingTxid: u.Txid, Amount: u.Amount, Type: domain.TxReceived, + IsPending: pending, CreatedAt: u.CreatedAt, }) } @@ -1874,80 +1879,39 @@ func (a *covenantArkClient) getBoardingTxs(ctx context.Context) (transactions [] } func vtxosToTxsCovenant( - roundLifetime int64, spendable, spent []client.Vtxo, boardingTxs []domain.Transaction, + roundLifetime int64, spendable, spent []client.Vtxo, ) ([]domain.Transaction, error) { - transactions := make([]domain.Transaction, 0) - unconfirmedBoardingTxs := make([]domain.Transaction, 0) - for _, tx := range boardingTxs { - emptyTime := time.Time{} - if tx.CreatedAt == emptyTime { - unconfirmedBoardingTxs = append(unconfirmedBoardingTxs, tx) - continue - } - transactions = append(transactions, tx) + txs := make([]domain.Transaction, 0) + + relatedVtxosBySpentBy := make(map[string][]client.Vtxo) + for _, v := range spent { + relatedVtxosBySpentBy[v.SpentBy] = append(relatedVtxosBySpentBy[v.SpentBy], v) } - for _, v := range append(spendable, spent...) { - // get vtxo amount - amount := int(v.Amount) - if !v.Pending { - continue - } - // find other spent vtxos that spent this one - relatedVtxos := findVtxosBySpentBy(spent, v.Txid) - for _, r := range relatedVtxos { - if r.Amount < math.MaxInt64 { - rAmount := int(r.Amount) - amount -= rAmount + for _, vtxo := range append(spendable, spent...) { + amount := int(vtxo.Amount) + + if relatedVtxos, ok := relatedVtxosBySpentBy[vtxo.RoundTxid]; ok { + for _, relatedVtxo := range relatedVtxos { + rAmount := int(relatedVtxo.Amount) + if rAmount < math.MaxInt64 { + amount -= rAmount + } } } - // what kind of tx was this? send or receive? + txType := domain.TxReceived if amount < 0 { txType = domain.TxSent } - // get redeem txid - redeemTxid := "" - if len(v.RedeemTx) > 0 { - txid, err := getRedeemTxidCovenant(v.RedeemTx) - if err != nil { - return nil, err - } - redeemTxid = txid - } - // add transaction - transactions = append(transactions, domain.Transaction{ - RoundTxid: v.RoundTxid, - RedeemTxid: redeemTxid, - Amount: uint64(math.Abs(float64(amount))), - Type: txType, - CreatedAt: getCreatedAtFromExpiry(roundLifetime, *v.ExpiresAt), - }) - } - - // Sort the slice by age - sort.Slice(transactions, func(i, j int) bool { - txi := transactions[i] - txj := transactions[j] - if txi.CreatedAt.Equal(txj.CreatedAt) { - return txi.Type > txj.Type - } - return txi.CreatedAt.After(txj.CreatedAt) - }) - - return append(unconfirmedBoardingTxs, transactions...), nil -} - -func getRedeemTxidCovenant(redeemTx string) (string, error) { - redeemPtx, err := psetv2.NewPsetFromBase64(redeemTx) - if err != nil { - return "", fmt.Errorf("failed to parse redeem tx: %s", err) - } - tx, err := redeemPtx.UnsignedTx() - if err != nil { - return "", fmt.Errorf("failed to get txid from redeem tx: %s", err) + txs = append(txs, domain.Transaction{ + RoundTxid: vtxo.RoundTxid, + Amount: uint64(math.Abs(float64(amount))), + Type: txType, + CreatedAt: getCreatedAtFromExpiry(roundLifetime, *vtxo.ExpiresAt), + }) } - return tx.TxHash().String(), nil + return txs, nil } diff --git a/pkg/client-sdk/covenant_client_test.go b/pkg/client-sdk/covenant_client_test.go new file mode 100644 index 000000000..d6b778dc0 --- /dev/null +++ b/pkg/client-sdk/covenant_client_test.go @@ -0,0 +1,187 @@ +package arksdk + +import ( + "context" + "encoding/hex" + "testing" + "time" + + "github.com/ark-network/ark/pkg/client-sdk/client" + "github.com/ark-network/ark/pkg/client-sdk/store" + "github.com/ark-network/ark/pkg/client-sdk/store/domain" + "github.com/decred/dcrd/dcrec/secp256k1/v4" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCovenantArkClientListenToVtxoChan(t *testing.T) { + var ( + ctx = context.Background() + err error + sdkRepo domain.SdkRepository + txs []domain.Transaction + spendableVtxosOld, spentVtxosOld []domain.Vtxo + ) + + sdkRepo, err = store.NewService(store.Config{ + ConfigStoreType: store.FileStore, + AppDataStoreType: store.Badger, + BaseDir: t.TempDir(), + }) + require.NoError(t, err) + by, err := hex.DecodeString("020000000000000000000000000000000000000000000000000000000000000001") + require.NoError(t, err) + aspPubkey, err := secp256k1.ParsePubKey(by) + + arkC := arkClient{ + ConfigData: &domain.ConfigData{ + AspPubkey: aspPubkey, + RoundInterval: 20, + }, + sdkRepository: sdkRepo, + sdkInitialized: false, + } + + covenantArkClient := &covenantArkClient{ + arkClient: &arkC, + } + + err = covenantArkClient.processVtxosAndTxs(ctx, nil, nil, []domain.Transaction{boardingTx1}) + require.NoError(t, err) + + txs, err = sdkRepo.AppDataRepository().TransactionRepository().GetAll(ctx) + require.NoError(t, err) + require.Len(t, txs, 1) + + spendableVtxosOld, spentVtxosOld, err = sdkRepo.AppDataRepository().VtxoRepository().GetAll(ctx) + require.NoError(t, err) + require.Len(t, spendableVtxosOld, 0) + require.Len(t, spentVtxosOld, 0) + + spendableVtxo := []client.Vtxo{vtxo1} + err = covenantArkClient.processVtxosAndTxs(ctx, spendableVtxo, nil, []domain.Transaction{boardingTx1}) + require.NoError(t, err) + + spentVtxo := []client.Vtxo{vtxo2} + err = covenantArkClient.processVtxosAndTxs(ctx, spendableVtxo, spentVtxo, []domain.Transaction{boardingTx1}) + require.NoError(t, err) +} + +var boardingTx1 = domain.Transaction{ + BoardingTxid: "ecba8b7280ceac8dfcc2c1bb34dd45c8783d09f970e9eb9d30ef436c91c036b6", + Amount: 3000, + Type: domain.TxReceived, + CreatedAt: time.Now(), +} + +var vtxo1 = client.Vtxo{ + Outpoint: client.Outpoint{ + Txid: "b8395c0fbc9cc6e56c172d6d3bebcf030fcc0bb5cf168361d515d62240e01010", + VOut: 0, + }, + Descriptor: "tr(0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,{ and(pk(2763c97adba0ba950b65cbb78f306661a502509251614a1f7805a9facbbc0d22), pk(8ec2a71e3fb19b27b06237ed8453c8eafa7e326d22338446a91d439975c4ed50)), and(older(1024), pk(2763c97adba0ba950b65cbb78f306661a502509251614a1f7805a9facbbc0d22)) })", + Amount: 99999000, + RoundTxid: "2af148e364f9e5e1dfd034ce8ac1a875ab3e341fef43ea29c551992a40150c20", + ExpiresAt: &time.Time{}, + SpentBy: "", +} + +var vtxo2 = client.Vtxo{ + Outpoint: client.Outpoint{ + Txid: "f17c9f987ae8061298a758dbdf7299793bc422244beb2c658e36c91e1f01bb7f", + VOut: 0, + }, + Descriptor: "tr(0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,{ and(pk(2763c97adba0ba950b65cbb78f306661a502509251614a1f7805a9facbbc0d22), pk(8ec2a71e3fb19b27b06237ed8453c8eafa7e326d22338446a91d439975c4ed50)), and(older(1024), pk(2763c97adba0ba950b65cbb78f306661a502509251614a1f7805a9facbbc0d22)) })", + Amount: 100000000, + RoundTxid: "ecba8b7280ceac8dfcc2c1bb34dd45c8783d09f970e9eb9d30ef436c91c036b6", + ExpiresAt: &time.Time{}, + SpentBy: "2af148e364f9e5e1dfd034ce8ac1a875ab3e341fef43ea29c551992a40150c20", +} + +func TestUpdateBoardingTxsState(t *testing.T) { + now := time.Now() + + testCases := []struct { + description string + allBoardingTxs []domain.Transaction + oldBoardingTxs []domain.Transaction + expectedNewTxs []domain.Transaction + expectedUpdatedTxs []domain.Transaction + }{ + { + description: "No boarding transactions in both lists", + allBoardingTxs: []domain.Transaction{}, + oldBoardingTxs: []domain.Transaction{}, + expectedNewTxs: []domain.Transaction{}, + expectedUpdatedTxs: []domain.Transaction{}, + }, + { + description: "All old boarding txs are still pending and present in new list", + allBoardingTxs: []domain.Transaction{ + {BoardingTxid: "tx1", IsPending: true, CreatedAt: now}, + {BoardingTxid: "tx2", IsPending: true, CreatedAt: now}, + }, + oldBoardingTxs: []domain.Transaction{ + {BoardingTxid: "tx1", IsPending: true, CreatedAt: now}, + {BoardingTxid: "tx2", IsPending: true, CreatedAt: now}, + }, + expectedNewTxs: []domain.Transaction{}, + expectedUpdatedTxs: []domain.Transaction{}, + }, + { + description: "Some old boarding txs not in new list (should be marked as pending=false)", + allBoardingTxs: []domain.Transaction{ + {BoardingTxid: "tx1", IsPending: true, CreatedAt: now}, + }, + oldBoardingTxs: []domain.Transaction{ + {BoardingTxid: "tx1", IsPending: true, CreatedAt: now}, + {BoardingTxid: "tx2", IsPending: true, CreatedAt: now}, + }, + expectedNewTxs: []domain.Transaction{}, + expectedUpdatedTxs: []domain.Transaction{ + {BoardingTxid: "tx2", IsPending: false, CreatedAt: now}, + }, + }, + { + description: "New boarding txs not present in old list", + allBoardingTxs: []domain.Transaction{ + {BoardingTxid: "tx1", IsPending: true, CreatedAt: now}, + {BoardingTxid: "tx3", IsPending: true, CreatedAt: now}, + }, + oldBoardingTxs: []domain.Transaction{ + {BoardingTxid: "tx1", IsPending: true, CreatedAt: now}, + }, + expectedNewTxs: []domain.Transaction{ + {BoardingTxid: "tx3", IsPending: true, CreatedAt: now}, + }, + expectedUpdatedTxs: []domain.Transaction{}, + }, + { + description: "No overlap between old and new boarding txs", + allBoardingTxs: []domain.Transaction{ + {BoardingTxid: "tx3", IsPending: true, CreatedAt: now}, + {BoardingTxid: "tx4", IsPending: true, CreatedAt: now}, + }, + oldBoardingTxs: []domain.Transaction{ + {BoardingTxid: "tx1", IsPending: true, CreatedAt: now}, + {BoardingTxid: "tx2", IsPending: true, CreatedAt: now}, + }, + expectedNewTxs: []domain.Transaction{ + {BoardingTxid: "tx3", IsPending: true, CreatedAt: now}, + {BoardingTxid: "tx4", IsPending: true, CreatedAt: now}, + }, + expectedUpdatedTxs: []domain.Transaction{ + {BoardingTxid: "tx1", IsPending: false, CreatedAt: now}, + {BoardingTxid: "tx2", IsPending: false, CreatedAt: now}, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + newBoardingTxs, updatedOldBoardingTxs := updateBoardingTxsState(tc.allBoardingTxs, tc.oldBoardingTxs) + assert.Equal(t, tc.expectedNewTxs, newBoardingTxs) + assert.Equal(t, tc.expectedUpdatedTxs, updatedOldBoardingTxs) + }) + } +} diff --git a/pkg/client-sdk/covenantless_client.go b/pkg/client-sdk/covenantless_client.go index 0933a1db9..ceb8b3c0c 100644 --- a/pkg/client-sdk/covenantless_client.go +++ b/pkg/client-sdk/covenantless_client.go @@ -6,7 +6,6 @@ import ( "encoding/hex" "fmt" "math" - "sort" "strings" "sync" "time" @@ -789,36 +788,16 @@ func (a *covenantlessArkClient) listenToVtxoChan(ctnx context.Context) error { continue } - spendableVtxosOld, spentVtxosOld, err := a.sdkRepository. - AppDataRepository().VtxoRepository().GetAll(ctx) - if err != nil { - log.Errorf("failed to get vtxos: %s", err) - continue - } - allBoardingTxs := a.getBoardingTxs(ctx) - oldVtxos := append(spendableVtxosOld, spentVtxosOld...) - if len(oldVtxos) == 0 { - err = a.insertInitialTxsAndVtxos( - ctx, - allSpendableVtxos, - allSpentVtxos, - allBoardingTxs, - ) - if err != nil { - log.Errorf("failed to process new vtxos: %s", err) - } - } else { - err := a.insertNewTxsAndVtxos( - ctx, - allSpendableVtxos, - allSpentVtxos, - oldVtxos, - allBoardingTxs, - ) - if err != nil { - log.Errorf("failed to update vtxos: %s", err) - } + + if err := a.processVtxosAndTxs( + ctx, + allSpendableVtxos, + allSpentVtxos, + allBoardingTxs, + ); err != nil { + log.Errorf("failed to process vtxos: %s", err) + continue } } } @@ -827,101 +806,123 @@ func (a *covenantlessArkClient) listenToVtxoChan(ctnx context.Context) error { return nil } -func (a *covenantlessArkClient) insertInitialTxsAndVtxos( +func (a *covenantlessArkClient) processVtxosAndTxs( ctx context.Context, - spendableVtxos, - spentVtxos []client.Vtxo, - boardingTxs []domain.Transaction, + allSpendableVtxos, + allSpentVtxos []client.Vtxo, + allBoardingTxs []domain.Transaction, ) error { - txs, err := vtxosToTxsCovenantless( - a.ConfigData.RoundInterval, - spendableVtxos, - spentVtxos, - boardingTxs, - ) + if err := a.processBoardingTxs(ctx, allBoardingTxs); err != nil { + return fmt.Errorf("failed to process txs: %s", err) + } + + return a.processVtxos(ctx, allSpendableVtxos, allSpentVtxos) +} + +func (a *covenantlessArkClient) processBoardingTxs( + ctx context.Context, + allBoardingTxs []domain.Transaction, +) error { + oldBoardingTxs, err := a.sdkRepository.AppDataRepository(). + TransactionRepository().GetBoardingTxs(ctx) if err != nil { - return err + return fmt.Errorf("failed to get boarding txs: %s", err) } - if len(txs) > 0 { + if len(oldBoardingTxs) == 0 { if err := a.sdkRepository.AppDataRepository().TransactionRepository(). - InsertTransactions(ctx, txs); err != nil { - return err + InsertTransactions(ctx, allBoardingTxs); err != nil { + return fmt.Errorf("failed to insert boarding txs: %s", err) + } + } else { + newBoardingTxs, updatedOldBoardingTxs := updateBoardingTxsState(allBoardingTxs, oldBoardingTxs) + if err := a.sdkRepository.AppDataRepository().TransactionRepository(). + InsertTransactions(ctx, newBoardingTxs); err != nil { + return fmt.Errorf("failed to insert boarding txs: %s", err) } - } - domainVtxos := append( - convertVtxosToDomainVtxos(spendableVtxos, false), - convertVtxosToDomainVtxos(spentVtxos, true)..., - ) - if len(domainVtxos) > 0 { - return a.sdkRepository.AppDataRepository().VtxoRepository(). - InsertVtxos(ctx, domainVtxos) + if err := a.sdkRepository.AppDataRepository().TransactionRepository(). + UpdateTransactions(ctx, updatedOldBoardingTxs); err != nil { + return fmt.Errorf("failed to update boarding txs: %s", err) + } } return nil } -func (a *covenantlessArkClient) insertNewTxsAndVtxos( +func (a *covenantlessArkClient) processVtxos( ctx context.Context, allSpendableVtxos, allSpentVtxos []client.Vtxo, - oldVtxos []domain.Vtxo, - allBoardingTxs []domain.Transaction, ) error { - allSpendableVtxosMap := make(map[string]client.Vtxo) - allSpentVtxosMap := make(map[string]client.Vtxo) - oldVtxosMap := make(map[string]domain.Vtxo) - - for _, vtxo := range allSpendableVtxos { - key := fmt.Sprintf("%s:%d", vtxo.Txid, vtxo.VOut) - allSpendableVtxosMap[key] = vtxo - } - - for _, vtxo := range allSpentVtxos { - key := fmt.Sprintf("%s:%d", vtxo.Txid, vtxo.VOut) - allSpentVtxosMap[key] = vtxo - } - - for _, vtxo := range oldVtxos { - key := fmt.Sprintf("%s:%d", vtxo.Txid, vtxo.VOut) - oldVtxosMap[key] = vtxo - } - - newSpendableVtxos := getNewVtxos(allSpendableVtxosMap, oldVtxosMap) - newSpentVtxos := getNewVtxos(allSpentVtxosMap, oldVtxosMap) - - oldBoardingTxs, err := a.sdkRepository.AppDataRepository(). - TransactionRepository().GetBoardingTxs(ctx) + spendableVtxosOld, spentVtxosOld, err := a.sdkRepository. + AppDataRepository().VtxoRepository().GetAll(ctx) if err != nil { - return err + return fmt.Errorf("failed to get vtxos: %s", err) } - newBoardingTxs := filterNewBoardingTxs(allBoardingTxs, oldBoardingTxs) + oldVtxos := append(spendableVtxosOld, spentVtxosOld...) - txs, err := vtxosToTxsCovenantless( + allTxs, err := vtxosToTxsCovenantless( a.ConfigData.RoundInterval, - newSpendableVtxos, - newSpentVtxos, - newBoardingTxs, + allSpendableVtxos, + allSpentVtxos, ) if err != nil { return err } + if len(oldVtxos) == 0 { + err = a.insertVtxosAndTransactions( + ctx, + allTxs, + allSpendableVtxos, + allSpentVtxos, + ) + if err != nil { + return fmt.Errorf("failed to process new vtxos: %s", err) + } + } else { + oldTxs, err := a.sdkRepository.AppDataRepository().TransactionRepository().GetAll(ctx) + if err != nil { + return fmt.Errorf("failed to get old transactions: %s", err) + } - if len(txs) > 0 { - if err := a.sdkRepository.AppDataRepository().TransactionRepository(). - InsertTransactions(ctx, txs); err != nil { - return err + newTxs, err := findNewTxs(oldTxs, allTxs) + err = a.insertVtxosAndTransactions( + ctx, + newTxs, + allSpendableVtxos, + allSpentVtxos, + ) + if err != nil { + return fmt.Errorf("failed to process new vtxos: %s", err) } } - domainSpendableVtxos := convertVtxosToDomainVtxos(newSpendableVtxos, false) - domainSpentVtxos := convertVtxosToDomainVtxos(newSpentVtxos, true) + return nil +} + +func (a *covenantlessArkClient) insertVtxosAndTransactions( + ctx context.Context, + txs []domain.Transaction, + spendableVtxos []client.Vtxo, + spentVtxos []client.Vtxo, +) error { + if err := a.sdkRepository.AppDataRepository().TransactionRepository(). + InsertTransactions(ctx, txs); err != nil { + return err + } + + domainVtxos := convertVtxosToDomainVtxos(spendableVtxos, false) + domainVtxos = append(domainVtxos, convertVtxosToDomainVtxos(spentVtxos, true)...) + + if len(domainVtxos) > 0 { + if err := a.sdkRepository.AppDataRepository().VtxoRepository().DeleteAll(ctx); err != nil { + return err + } - if len(domainSpendableVtxos) > 0 || len(domainSpentVtxos) > 0 { return a.sdkRepository.AppDataRepository().VtxoRepository(). - InsertVtxos(ctx, append(domainSpentVtxos, domainSpendableVtxos...)) + InsertVtxos(ctx, domainVtxos) } return nil @@ -2230,19 +2231,9 @@ func findVtxosBySpentBy(allVtxos []client.Vtxo, txid string) (vtxos []client.Vtx } func vtxosToTxsCovenantless( - roundLifetime int64, spendable, spent []client.Vtxo, boardingTxs []domain.Transaction, + roundLifetime int64, spendable, spent []client.Vtxo, ) ([]domain.Transaction, error) { - transactions := make([]domain.Transaction, 0) - unconfirmedBoardingTxs := make([]domain.Transaction, 0) - for _, tx := range boardingTxs { - emptyTime := time.Time{} - if tx.CreatedAt == emptyTime { - unconfirmedBoardingTxs = append(unconfirmedBoardingTxs, tx) - continue - } - transactions = append(transactions, tx) - } - + txs := make([]domain.Transaction, 0) for _, v := range append(spendable, spent...) { // get vtxo amount amount := int(v.Amount) @@ -2274,7 +2265,7 @@ func vtxosToTxsCovenantless( redeemTxid = txid } // add transaction - transactions = append(transactions, domain.Transaction{ + txs = append(txs, domain.Transaction{ RoundTxid: v.RoundTxid, RedeemTxid: redeemTxid, Amount: uint64(math.Abs(float64(amount))), @@ -2285,17 +2276,7 @@ func vtxosToTxsCovenantless( }) } - // Sort the slice by age - sort.Slice(transactions, func(i, j int) bool { - txi := transactions[i] - txj := transactions[j] - if txi.CreatedAt.Equal(txj.CreatedAt) { - return txi.Type > txj.Type - } - return txi.CreatedAt.After(txj.CreatedAt) - }) - - return append(unconfirmedBoardingTxs, transactions...), nil + return txs, nil } func getRedeemTxidCovenantless(redeemTx string) (string, error) { diff --git a/pkg/client-sdk/example/covenant/alice_to_bob.go b/pkg/client-sdk/example/covenant/alice_to_bob.go index d5062fb92..8f0323db5 100644 --- a/pkg/client-sdk/example/covenant/alice_to_bob.go +++ b/pkg/client-sdk/example/covenant/alice_to_bob.go @@ -118,7 +118,7 @@ func main() { log.Fatal(err) } - time.Sleep(240 * time.Second) + time.Sleep(5 * time.Second) aliceBalance, err = aliceArkClient.Balance(ctx, false) if err != nil { diff --git a/pkg/client-sdk/store/badger/transaction_repository.go b/pkg/client-sdk/store/badger/transaction_repository.go index e675ba7ea..7d1386834 100644 --- a/pkg/client-sdk/store/badger/transaction_repository.go +++ b/pkg/client-sdk/store/badger/transaction_repository.go @@ -4,10 +4,10 @@ import ( "context" "fmt" "path/filepath" + "sort" "github.com/ark-network/ark/pkg/client-sdk/store/domain" "github.com/dgraph-io/badger/v4" - "github.com/google/uuid" "github.com/timshannon/badgerhold/v4" ) @@ -40,10 +40,12 @@ func (t *transactionRepository) GetBoardingTxs(ctx context.Context) ([]domain.Tr return txs, err } -func (t *transactionRepository) InsertTransactions(ctx context.Context, txs []domain.Transaction) error { +func (t *transactionRepository) InsertTransactions( + ctx context.Context, + txs []domain.Transaction, +) error { for _, tx := range txs { - tx.ID = uuid.New().String() - if err := t.db.Insert(tx.ID, &tx); err != nil { + if err := t.db.Insert(tx.Key(), &tx); err != nil { return err } go func(trx domain.Transaction) { @@ -53,9 +55,35 @@ func (t *transactionRepository) InsertTransactions(ctx context.Context, txs []do return nil } +func (t *transactionRepository) UpdateTransactions( + ctx context.Context, + txs []domain.Transaction, +) error { + for _, tx := range txs { + if err := t.db.Upsert(tx.Key(), &tx); err != nil { + return err + } + go func(trx domain.Transaction) { + // TODO rethink, here we can track diff kind of events, like boarding claimed etc + t.eventCh <- trx + }(tx) + } + return nil +} + func (t *transactionRepository) GetAll(ctx context.Context) ([]domain.Transaction, error) { var txs []domain.Transaction err := t.db.Find(&txs, nil) + + sort.Slice(txs, func(i, j int) bool { + txi := txs[i] + txj := txs[j] + if txi.CreatedAt.Equal(txj.CreatedAt) { + return txi.Type > txj.Type + } + return txi.CreatedAt.After(txj.CreatedAt) + }) + return txs, err } diff --git a/pkg/client-sdk/store/badger/vtxo_repository.go b/pkg/client-sdk/store/badger/vtxo_repository.go index 982be9d7d..17037d8a1 100644 --- a/pkg/client-sdk/store/badger/vtxo_repository.go +++ b/pkg/client-sdk/store/badger/vtxo_repository.go @@ -55,3 +55,10 @@ func (v *vtxoRepository) GetAll( } return } + +func (v *vtxoRepository) DeleteAll(ctx context.Context) error { + if err := v.db.DeleteMatching(&domain.Vtxo{}, nil); err != nil { + return fmt.Errorf("failed to delete all vtxos: %w", err) + } + return nil +} diff --git a/pkg/client-sdk/store/domain/domain.go b/pkg/client-sdk/store/domain/domain.go index 3cc3c213a..6ec9aa6c1 100644 --- a/pkg/client-sdk/store/domain/domain.go +++ b/pkg/client-sdk/store/domain/domain.go @@ -1,6 +1,7 @@ package domain import ( + "fmt" "strconv" "time" @@ -47,7 +48,6 @@ const ( type TxType string type Transaction struct { - ID string BoardingTxid string RoundTxid string RedeemTxid string @@ -57,3 +57,11 @@ type Transaction struct { IsPendingChange bool CreatedAt time.Time } + +func (t Transaction) Key() string { + return fmt.Sprintf("%s:%s:%s", t.BoardingTxid, t.RoundTxid, t.RedeemTxid) +} + +func (t Transaction) IsBoarding() bool { + return t.BoardingTxid != "" +} diff --git a/pkg/client-sdk/store/domain/repository.go b/pkg/client-sdk/store/domain/repository.go index f140657b4..cd72d39e9 100644 --- a/pkg/client-sdk/store/domain/repository.go +++ b/pkg/client-sdk/store/domain/repository.go @@ -24,6 +24,7 @@ type ConfigRepository interface { type TransactionRepository interface { InsertTransactions(ctx context.Context, txs []Transaction) error + UpdateTransactions(ctx context.Context, txs []Transaction) error GetAll(ctx context.Context) ([]Transaction, error) GetEventChannel() chan Transaction GetBoardingTxs(ctx context.Context) ([]Transaction, error) @@ -33,4 +34,5 @@ type TransactionRepository interface { type VtxoRepository interface { InsertVtxos(ctx context.Context, vtxos []Vtxo) error GetAll(ctx context.Context) (spendable []Vtxo, spent []Vtxo, err error) + DeleteAll(ctx context.Context) error } diff --git a/pkg/client-sdk/store/service_test.go b/pkg/client-sdk/store/service_test.go index f527065f7..752e320a8 100644 --- a/pkg/client-sdk/store/service_test.go +++ b/pkg/client-sdk/store/service_test.go @@ -120,13 +120,13 @@ func TestNewService(t *testing.T) { testTxs := []domain.Transaction{ { - ID: "tx1", + RoundTxid: "tx1", Amount: 1000, Type: domain.TxSent, CreatedAt: time.Now(), }, { - ID: "tx2", + RoundTxid: "tx2", Amount: 2000, Type: domain.TxReceived, CreatedAt: time.Now(), @@ -163,5 +163,12 @@ func TestNewService(t *testing.T) { require.Len(t, spendable, 1) require.Len(t, spent, 1) + err = vtxoRepo.DeleteAll(ctx) + require.NoError(t, err) + + spendable, spent, err = vtxoRepo.GetAll(ctx) + require.NoError(t, err) + require.Len(t, spendable, 0) + service.AppDataRepository().Stop() } From 9f378786731a880641ff2ff91b609d4a62b74d2f Mon Sep 17 00:00:00 2001 From: sekulicd Date: Thu, 26 Sep 2024 10:42:09 +0200 Subject: [PATCH 12/15] arksdk close func, cli transaction cmd --- client/main.go | 25 +++++++++++++++++++ pkg/client-sdk/ark_sdk.go | 1 + pkg/client-sdk/client.go | 12 +++++++++ pkg/client-sdk/covenant_client.go | 13 +++++++--- pkg/client-sdk/covenantless_client.go | 13 +++++++--- .../store/badger/app_data_repository.go | 8 ++++-- .../store/badger/transaction_repository.go | 7 +++++- .../store/badger/vtxo_repository.go | 4 +++ pkg/client-sdk/store/domain/repository.go | 5 ++-- 9 files changed, 77 insertions(+), 11 deletions(-) diff --git a/client/main.go b/client/main.go index 52cc35ce0..b946f4631 100644 --- a/client/main.go +++ b/client/main.go @@ -42,6 +42,7 @@ func main() { &sendCommand, &balanceCommand, &redeemCommand, + $trasactionCommand, ) app.Flags = []cli.Flag{ datadirFlag, @@ -57,6 +58,15 @@ func main() { return nil } + app.After = func(ctx *cli.Context) error { + if arkSdkClient != nil { + if err := arkSdkClient.Close(); err != nil { + return fmt.Errorf("error closing ark sdk client: %v", err) + } + } + return nil + } + err := app.Run(os.Args) if err != nil { fmt.Println(fmt.Errorf("error: %v", err)) @@ -191,6 +201,13 @@ var ( return redeem(ctx) }, } + transactionCommand = cli.Command{ + Name: "transactions", + Usage: "Show transaction history", + Action: func(ctx *cli.Context) error { + return listTxs(ctx) + }, + } ) func initArkSdk(ctx *cli.Context) error { @@ -381,6 +398,14 @@ func redeem(ctx *cli.Context) error { }) } +func listTxs(ctx *cli.Context) error { + txs, err := arkSdkClient.GetTransactionHistory(ctx.Context) + if err != nil { + return err + } + return printJSON(txs) +} + func getArkSdkClient(ctx *cli.Context) (arksdk.ArkClient, error) { dataDir := ctx.String(datadirFlag.Name) sdkRepository, err := getSdkRepository(dataDir) diff --git a/pkg/client-sdk/ark_sdk.go b/pkg/client-sdk/ark_sdk.go index 07cf1d69b..2519f1e2d 100644 --- a/pkg/client-sdk/ark_sdk.go +++ b/pkg/client-sdk/ark_sdk.go @@ -30,6 +30,7 @@ type ArkClient interface { Dump(ctx context.Context) (seed string, err error) GetTransactionHistory(ctx context.Context) ([]domain.Transaction, error) GetTransactionEventChannel() chan domain.Transaction + Close() error } type Receiver interface { diff --git a/pkg/client-sdk/client.go b/pkg/client-sdk/client.go index b29362882..54a0e11c1 100644 --- a/pkg/client-sdk/client.go +++ b/pkg/client-sdk/client.go @@ -61,6 +61,8 @@ const ( type spent bool type arkClient struct { + ctxCancelFunc context.CancelFunc + *domain.ConfigData sdkRepository domain.SdkRepository wallet wallet.WalletService @@ -249,6 +251,16 @@ func (a *arkClient) Receive(ctx context.Context) (string, string, error) { return offchainAddr, boardingAddr, nil } +func (a *arkClient) Close() error { + if err := a.sdkRepository.AppDataRepository().Stop(); err != nil { + return err + } + + a.ctxCancelFunc() + + return nil +} + func (a *arkClient) ping( ctx context.Context, paymentID string, ) func() { diff --git a/pkg/client-sdk/covenant_client.go b/pkg/client-sdk/covenant_client.go index c43ae8910..c81e3cbee 100644 --- a/pkg/client-sdk/covenant_client.go +++ b/pkg/client-sdk/covenant_client.go @@ -65,13 +65,15 @@ func NewCovenantClient( return nil, ErrAlreadyInitialized } + ctx, ctxCancel := context.WithCancel(context.Background()) cvnt := &covenantArkClient{ arkClient: &arkClient{ + ctxCancelFunc: ctxCancel, sdkRepository: sdkRepository, }, } - if err := cvnt.listenToVtxoChan(context.Background()); err != nil { + if err := cvnt.listenToVtxoChan(ctx); err != nil { return nil, err } @@ -110,8 +112,10 @@ func LoadCovenantClient( return nil, fmt.Errorf("faile to setup wallet: %s", err) } + ctx, ctxCancel := context.WithCancel(context.Background()) cvnt := &covenantArkClient{ &arkClient{ + ctxCancelFunc: ctxCancel, ConfigData: data, wallet: walletSvc, sdkRepository: sdkRepository, @@ -120,7 +124,7 @@ func LoadCovenantClient( }, } - if err := cvnt.listenToVtxoChan(context.Background()); err != nil { + if err := cvnt.listenToVtxoChan(ctx); err != nil { return nil, err } @@ -158,8 +162,10 @@ func LoadCovenantClientWithWallet( return nil, fmt.Errorf("failed to setup explorer: %s", err) } + ctx, ctxCancel := context.WithCancel(context.Background()) cvnt := &covenantArkClient{ &arkClient{ + ctxCancelFunc: ctxCancel, ConfigData: data, wallet: walletSvc, sdkRepository: sdkRepository, @@ -168,7 +174,7 @@ func LoadCovenantClientWithWallet( }, } - if err := cvnt.listenToVtxoChan(context.Background()); err != nil { + if err := cvnt.listenToVtxoChan(ctx); err != nil { return nil, err } @@ -602,6 +608,7 @@ func (a *covenantArkClient) listenToVtxoChan(ctnx context.Context) error { select { case <-ctx.Done(): + log.Info("stopping listening to vtxos") return case <-ticker.C: allSpendableVtxos, allSpentVtxos, err := a.ListVtxos(ctx) diff --git a/pkg/client-sdk/covenantless_client.go b/pkg/client-sdk/covenantless_client.go index ceb8b3c0c..05517a5bb 100644 --- a/pkg/client-sdk/covenantless_client.go +++ b/pkg/client-sdk/covenantless_client.go @@ -67,13 +67,15 @@ func NewCovenantlessClient( return nil, ErrAlreadyInitialized } + ctx, ctxCancel := context.WithCancel(context.Background()) cvnt := &covenantlessArkClient{ arkClient: &arkClient{ + ctxCancelFunc: ctxCancel, sdkRepository: sdkRepository, }, } - if err := cvnt.listenToVtxoChan(context.Background()); err != nil { + if err := cvnt.listenToVtxoChan(ctx); err != nil { return nil, err } @@ -112,8 +114,10 @@ func LoadCovenantlessClient( return nil, fmt.Errorf("faile to setup wallet: %s", err) } + ctx, ctxCancel := context.WithCancel(context.Background()) cvnt := &covenantlessArkClient{ &arkClient{ + ctxCancelFunc: ctxCancel, ConfigData: data, wallet: walletSvc, sdkRepository: sdkRepository, @@ -122,7 +126,7 @@ func LoadCovenantlessClient( }, } - if err := cvnt.listenToVtxoChan(context.Background()); err != nil { + if err := cvnt.listenToVtxoChan(ctx); err != nil { return nil, err } @@ -160,8 +164,10 @@ func LoadCovenantlessClientWithWallet( return nil, fmt.Errorf("failed to setup explorer: %s", err) } + ctx, ctxCancel := context.WithCancel(context.Background()) cvnt := &covenantlessArkClient{ &arkClient{ + ctxCancelFunc: ctxCancel, ConfigData: data, wallet: walletSvc, sdkRepository: sdkRepository, @@ -170,7 +176,7 @@ func LoadCovenantlessClientWithWallet( }, } - if err := cvnt.listenToVtxoChan(context.Background()); err != nil { + if err := cvnt.listenToVtxoChan(ctx); err != nil { return nil, err } @@ -780,6 +786,7 @@ func (a *covenantlessArkClient) listenToVtxoChan(ctnx context.Context) error { select { case <-ctx.Done(): + log.Info("stopping listening to vtxos") return case <-ticker.C: allSpendableVtxos, allSpentVtxos, err := a.ListVtxos(ctx) diff --git a/pkg/client-sdk/store/badger/app_data_repository.go b/pkg/client-sdk/store/badger/app_data_repository.go index 4f8ef092c..49b28a207 100644 --- a/pkg/client-sdk/store/badger/app_data_repository.go +++ b/pkg/client-sdk/store/badger/app_data_repository.go @@ -27,6 +27,10 @@ func (a *appDataRepository) VtxoRepository() domain.VtxoRepository { return a.vtxoRepository } -func (a *appDataRepository) Stop() { - a.transactionRepository.Stop() +func (a *appDataRepository) Stop() error { + if err := a.transactionRepository.Stop(); err != nil { + return err + } + + return a.vtxoRepository.Stop() } diff --git a/pkg/client-sdk/store/badger/transaction_repository.go b/pkg/client-sdk/store/badger/transaction_repository.go index 7d1386834..c85b0ad52 100644 --- a/pkg/client-sdk/store/badger/transaction_repository.go +++ b/pkg/client-sdk/store/badger/transaction_repository.go @@ -91,6 +91,11 @@ func (t *transactionRepository) GetEventChannel() chan domain.Transaction { return t.eventCh } -func (t *transactionRepository) Stop() { +func (t *transactionRepository) Stop() error { + if err := t.db.Close(); err != nil { + return err + } close(t.eventCh) + + return nil } diff --git a/pkg/client-sdk/store/badger/vtxo_repository.go b/pkg/client-sdk/store/badger/vtxo_repository.go index 17037d8a1..452b4296a 100644 --- a/pkg/client-sdk/store/badger/vtxo_repository.go +++ b/pkg/client-sdk/store/badger/vtxo_repository.go @@ -62,3 +62,7 @@ func (v *vtxoRepository) DeleteAll(ctx context.Context) error { } return nil } + +func (v *vtxoRepository) Stop() error { + return v.db.Close() +} diff --git a/pkg/client-sdk/store/domain/repository.go b/pkg/client-sdk/store/domain/repository.go index cd72d39e9..432685389 100644 --- a/pkg/client-sdk/store/domain/repository.go +++ b/pkg/client-sdk/store/domain/repository.go @@ -11,7 +11,7 @@ type AppDataRepository interface { TransactionRepository() TransactionRepository VtxoRepository() VtxoRepository - Stop() + Stop() error } type ConfigRepository interface { @@ -28,11 +28,12 @@ type TransactionRepository interface { GetAll(ctx context.Context) ([]Transaction, error) GetEventChannel() chan Transaction GetBoardingTxs(ctx context.Context) ([]Transaction, error) - Stop() + Stop() error } type VtxoRepository interface { InsertVtxos(ctx context.Context, vtxos []Vtxo) error GetAll(ctx context.Context) (spendable []Vtxo, spent []Vtxo, err error) DeleteAll(ctx context.Context) error + Stop() error } From 34c2b2710e9275ec3664e7d355467b92bca5f609 Mon Sep 17 00:00:00 2001 From: sekulicd Date: Thu, 26 Sep 2024 10:42:56 +0200 Subject: [PATCH 13/15] fix --- client/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/main.go b/client/main.go index b946f4631..7c1acde8e 100644 --- a/client/main.go +++ b/client/main.go @@ -42,7 +42,7 @@ func main() { &sendCommand, &balanceCommand, &redeemCommand, - $trasactionCommand, + &transactionCommand, ) app.Flags = []cli.Flag{ datadirFlag, From d5bcbf0e8b7ccd1cf84e41ff69f25c11ebcae37b Mon Sep 17 00:00:00 2001 From: sekulicd Date: Thu, 26 Sep 2024 15:25:50 +0200 Subject: [PATCH 14/15] fixes --- client/main.go | 35 +- pkg/client-sdk/ark_sdk.go | 1 + pkg/client-sdk/client.go | 31 +- pkg/client-sdk/covenant_client.go | 167 +++----- pkg/client-sdk/covenantless_client.go | 389 ++++++++++-------- .../store/badger/app_data_repository.go | 9 +- .../store/badger/vtxo_repository.go | 68 --- pkg/client-sdk/store/domain/repository.go | 8 - pkg/client-sdk/store/service.go | 10 - 9 files changed, 293 insertions(+), 425 deletions(-) delete mode 100644 pkg/client-sdk/store/badger/vtxo_repository.go diff --git a/client/main.go b/client/main.go index 7c1acde8e..654895b88 100644 --- a/client/main.go +++ b/client/main.go @@ -54,7 +54,6 @@ func main() { return fmt.Errorf("error initializing ark sdk client: %v", err) } arkSdkClient = sdk - return nil } @@ -229,12 +228,7 @@ func initArkSdk(ctx *cli.Context) error { } func config(ctx *cli.Context) error { - sdkRepository, err := getSdkRepository(ctx.String(datadirFlag.Name)) - if err != nil { - return err - } - - cfgData, err := sdkRepository.ConfigRepository().GetData(ctx.Context) + cfgData, err := arkSdkClient.GetConfigData(ctx.Context) if err != nil { return err } @@ -317,12 +311,12 @@ func send(ctx *cli.Context) error { return fmt.Errorf("missing destination, use --to and --amount or --receivers") } - sdkRepository, err := getSdkRepository(ctx.String(datadirFlag.Name)) + configData, err := arkSdkClient.GetConfigData(ctx.Context) if err != nil { return err } - net, err := getNetwork(ctx, sdkRepository) + net, err := getNetwork(ctx, configData) if err != nil { return err } @@ -408,7 +402,11 @@ func listTxs(ctx *cli.Context) error { func getArkSdkClient(ctx *cli.Context) (arksdk.ArkClient, error) { dataDir := ctx.String(datadirFlag.Name) - sdkRepository, err := getSdkRepository(dataDir) + sdkRepository, err := store.NewService(store.Config{ + ConfigStoreType: store.FileStore, + AppDataStoreType: store.Badger, + BaseDir: dataDir, + }) if err != nil { return nil, err } @@ -423,7 +421,7 @@ func getArkSdkClient(ctx *cli.Context) (arksdk.ArkClient, error) { return nil, fmt.Errorf("CLI not initialized, run 'init' cmd to initialize") } - net, err := getNetwork(ctx, sdkRepository) + net, err := getNetwork(ctx, configData) if err != nil { return nil, err } @@ -452,20 +450,7 @@ func loadOrCreateClient( return client, err } -func getSdkRepository(dataDir string) (domain.SdkRepository, error) { - return store.NewService(store.Config{ - ConfigStoreType: store.FileStore, - AppDataStoreType: store.Badger, - BaseDir: dataDir, - }) -} - -func getNetwork(ctx *cli.Context, sdkRepository domain.SdkRepository) (string, error) { - configData, err := sdkRepository.ConfigRepository().GetData(context.Background()) - if err != nil { - return "", err - } - +func getNetwork(ctx *cli.Context, configData *domain.ConfigData) (string, error) { if configData == nil { return ctx.String(networkFlag.Name), nil } diff --git a/pkg/client-sdk/ark_sdk.go b/pkg/client-sdk/ark_sdk.go index 2519f1e2d..2281a4711 100644 --- a/pkg/client-sdk/ark_sdk.go +++ b/pkg/client-sdk/ark_sdk.go @@ -31,6 +31,7 @@ type ArkClient interface { GetTransactionHistory(ctx context.Context) ([]domain.Transaction, error) GetTransactionEventChannel() chan domain.Transaction Close() error + ListenToVtxoChan() error } type Receiver interface { diff --git a/pkg/client-sdk/client.go b/pkg/client-sdk/client.go index 54a0e11c1..489fc0ec5 100644 --- a/pkg/client-sdk/client.go +++ b/pkg/client-sdk/client.go @@ -61,7 +61,8 @@ const ( type spent bool type arkClient struct { - ctxCancelFunc context.CancelFunc + ctxListenVtxo context.Context + ctxCancelListenVtxo context.CancelFunc *domain.ConfigData sdkRepository domain.SdkRepository @@ -69,7 +70,8 @@ type arkClient struct { explorer explorer.Explorer client client.ASPClient - sdkInitialized bool + sdkInitialized bool + listeningToVtxo bool } func (a *arkClient) GetConfigData( @@ -252,12 +254,14 @@ func (a *arkClient) Receive(ctx context.Context) (string, string, error) { } func (a *arkClient) Close() error { + if a.listeningToVtxo { + a.ctxCancelListenVtxo() + } + if err := a.sdkRepository.AppDataRepository().Stop(); err != nil { return err } - a.ctxCancelFunc() - return nil } @@ -395,22 +399,3 @@ func containsTx(txs []domain.Transaction, txid string) bool { } return false } - -func convertVtxosToDomainVtxos(vtxos []client.Vtxo, spent bool) []domain.Vtxo { - domainVtxos := make([]domain.Vtxo, len(vtxos)) - for i, v := range vtxos { - domainVtxos[i] = domain.Vtxo{ - Txid: v.Txid, - VOut: v.VOut, - Amount: v.Amount, - RoundTxid: v.RoundTxid, - ExpiresAt: v.ExpiresAt, - RedeemTx: v.RedeemTx, - UnconditionalForfeitTxs: v.UnconditionalForfeitTxs, - Pending: v.Pending, - SpentBy: v.SpentBy, - Spent: spent, - } - } - return domainVtxos -} diff --git a/pkg/client-sdk/covenant_client.go b/pkg/client-sdk/covenant_client.go index c81e3cbee..895f9c06e 100644 --- a/pkg/client-sdk/covenant_client.go +++ b/pkg/client-sdk/covenant_client.go @@ -65,18 +65,15 @@ func NewCovenantClient( return nil, ErrAlreadyInitialized } - ctx, ctxCancel := context.WithCancel(context.Background()) + ctxListenVtxo, ctxCancelListenVtxo := context.WithCancel(context.Background()) cvnt := &covenantArkClient{ arkClient: &arkClient{ - ctxCancelFunc: ctxCancel, - sdkRepository: sdkRepository, + ctxListenVtxo: ctxListenVtxo, + ctxCancelListenVtxo: ctxCancelListenVtxo, + sdkRepository: sdkRepository, }, } - if err := cvnt.listenToVtxoChan(ctx); err != nil { - return nil, err - } - return cvnt, nil } @@ -112,22 +109,19 @@ func LoadCovenantClient( return nil, fmt.Errorf("faile to setup wallet: %s", err) } - ctx, ctxCancel := context.WithCancel(context.Background()) + ctxListenVtxo, ctxCancelListenVtxo := context.WithCancel(context.Background()) cvnt := &covenantArkClient{ &arkClient{ - ctxCancelFunc: ctxCancel, - ConfigData: data, - wallet: walletSvc, - sdkRepository: sdkRepository, - explorer: explorerSvc, - client: clientSvc, + ctxListenVtxo: ctxListenVtxo, + ctxCancelListenVtxo: ctxCancelListenVtxo, + ConfigData: data, + wallet: walletSvc, + sdkRepository: sdkRepository, + explorer: explorerSvc, + client: clientSvc, }, } - if err := cvnt.listenToVtxoChan(ctx); err != nil { - return nil, err - } - return cvnt, nil } @@ -162,22 +156,19 @@ func LoadCovenantClientWithWallet( return nil, fmt.Errorf("failed to setup explorer: %s", err) } - ctx, ctxCancel := context.WithCancel(context.Background()) + ctxListenVtxo, ctxCancelListenVtxo := context.WithCancel(context.Background()) cvnt := &covenantArkClient{ &arkClient{ - ctxCancelFunc: ctxCancel, - ConfigData: data, - wallet: walletSvc, - sdkRepository: sdkRepository, - explorer: explorerSvc, - client: clientSvc, + ctxListenVtxo: ctxListenVtxo, + ctxCancelListenVtxo: ctxCancelListenVtxo, + ConfigData: data, + wallet: walletSvc, + sdkRepository: sdkRepository, + explorer: explorerSvc, + client: clientSvc, }, } - if err := cvnt.listenToVtxoChan(ctx); err != nil { - return nil, err - } - return cvnt, nil } @@ -596,11 +587,43 @@ func (a *covenantArkClient) GetTransactionEventChannel() chan domain.Transaction return a.sdkRepository.AppDataRepository().TransactionRepository().GetEventChannel() } -func (a *covenantArkClient) listenToVtxoChan(ctnx context.Context) error { +func (a *covenantArkClient) ListenToVtxoChan() error { + a.listeningToVtxo = true + var wg sync.WaitGroup + go func(ctx context.Context) { - ticker := time.NewTicker(1 * time.Second) + ticker := time.NewTicker(2 * time.Second) defer ticker.Stop() + performAction := func() { + wg.Add(1) + defer wg.Done() + // Use a context that won't be canceled prematurely by arkClient ctx + ct := context.Background() + + allSpendableVtxos, allSpentVtxos, err := a.ListVtxos(ct) + if err != nil { + log.Errorf("failed to list vtxos: %s", err) + return + } + + allBoardingTxs := a.getBoardingTxs(ct) + + if err := a.processVtxosAndTxs( + ct, + allSpendableVtxos, + allSpentVtxos, + allBoardingTxs, + ); err != nil { + log.Errorf("failed to process vtxos: %s", err) + return + } + log.Info("processed vtxos") + } + + // initial action to prevent sync issue with immediate ctx cancel + performAction() + for { if !a.sdkInitialized { continue @@ -609,28 +632,13 @@ func (a *covenantArkClient) listenToVtxoChan(ctnx context.Context) error { select { case <-ctx.Done(): log.Info("stopping listening to vtxos") + wg.Wait() return case <-ticker.C: - allSpendableVtxos, allSpentVtxos, err := a.ListVtxos(ctx) - if err != nil { - log.Errorf("failed to list vtxos: %s", err) - continue - } - - allBoardingTxs := a.getBoardingTxs(ctx) - - if err := a.processVtxosAndTxs( - ctx, - allSpendableVtxos, - allSpentVtxos, - allBoardingTxs, - ); err != nil { - log.Errorf("failed to process vtxos: %s", err) - continue - } + performAction() } } - }(ctnx) + }(a.ctxListenVtxo) return nil } @@ -645,22 +653,15 @@ func (a *covenantArkClient) processVtxosAndTxs( return fmt.Errorf("failed to process txs: %s", err) } - return a.processVtxos(ctx, allSpendableVtxos, allSpentVtxos) + return a.processVtxos(ctx, allSpendableVtxos, allSpentVtxos, allBoardingTxs) } func (a *covenantArkClient) processVtxos( ctx context.Context, allSpendableVtxos, allSpentVtxos []client.Vtxo, + allBoardingTxs []domain.Transaction, ) error { - spendableVtxosOld, spentVtxosOld, err := a.sdkRepository. - AppDataRepository().VtxoRepository().GetAll(ctx) - if err != nil { - return fmt.Errorf("failed to get vtxos: %s", err) - } - - oldVtxos := append(spendableVtxosOld, spentVtxosOld...) - allTxs, err := vtxosToTxsCovenant( a.ConfigData.RoundInterval, allSpendableVtxos, @@ -669,15 +670,10 @@ func (a *covenantArkClient) processVtxos( if err != nil { return err } - if len(oldVtxos) == 0 { - err = a.insertVtxosAndTransactions( - ctx, - allTxs, - allSpendableVtxos, - allSpentVtxos, - ) - if err != nil { - return fmt.Errorf("failed to process new vtxos: %s", err) + if len(allBoardingTxs) == 0 { + if err := a.sdkRepository.AppDataRepository().TransactionRepository(). + InsertTransactions(ctx, allTxs); err != nil { + return fmt.Errorf("failed to insert txs: %s", err) } } else { oldTxs, err := a.sdkRepository.AppDataRepository().TransactionRepository().GetAll(ctx) @@ -686,14 +682,9 @@ func (a *covenantArkClient) processVtxos( } newTxs, err := findNewTxs(oldTxs, allTxs) - err = a.insertVtxosAndTransactions( - ctx, - newTxs, - allSpendableVtxos, - allSpentVtxos, - ) - if err != nil { - return fmt.Errorf("failed to process new vtxos: %s", err) + if err := a.sdkRepository.AppDataRepository().TransactionRepository(). + InsertTransactions(ctx, newTxs); err != nil { + return fmt.Errorf("failed to insert txs: %s", err) } } @@ -731,32 +722,6 @@ func (a *covenantArkClient) processBoardingTxs( return nil } -func (a *covenantArkClient) insertVtxosAndTransactions( - ctx context.Context, - txs []domain.Transaction, - spendableVtxos []client.Vtxo, - spentVtxos []client.Vtxo, -) error { - if err := a.sdkRepository.AppDataRepository().TransactionRepository(). - InsertTransactions(ctx, txs); err != nil { - return err - } - - domainVtxos := convertVtxosToDomainVtxos(spendableVtxos, false) - domainVtxos = append(domainVtxos, convertVtxosToDomainVtxos(spentVtxos, true)...) - - if len(domainVtxos) > 0 { - if err := a.sdkRepository.AppDataRepository().VtxoRepository().DeleteAll(ctx); err != nil { - return err - } - - return a.sdkRepository.AppDataRepository().VtxoRepository(). - InsertVtxos(ctx, domainVtxos) - } - - return nil -} - func (a *covenantArkClient) getAllBoardingUtxos(ctx context.Context) ([]explorer.Utxo, error) { _, boardingAddrs, _, err := a.wallet.GetAddresses(ctx) if err != nil { diff --git a/pkg/client-sdk/covenantless_client.go b/pkg/client-sdk/covenantless_client.go index 05517a5bb..63b1b49bb 100644 --- a/pkg/client-sdk/covenantless_client.go +++ b/pkg/client-sdk/covenantless_client.go @@ -67,18 +67,15 @@ func NewCovenantlessClient( return nil, ErrAlreadyInitialized } - ctx, ctxCancel := context.WithCancel(context.Background()) + ctxListenVtxo, ctxCancelListenVtxo := context.WithCancel(context.Background()) cvnt := &covenantlessArkClient{ arkClient: &arkClient{ - ctxCancelFunc: ctxCancel, - sdkRepository: sdkRepository, + ctxListenVtxo: ctxListenVtxo, + ctxCancelListenVtxo: ctxCancelListenVtxo, + sdkRepository: sdkRepository, }, } - if err := cvnt.listenToVtxoChan(ctx); err != nil { - return nil, err - } - return cvnt, nil } @@ -114,22 +111,19 @@ func LoadCovenantlessClient( return nil, fmt.Errorf("faile to setup wallet: %s", err) } - ctx, ctxCancel := context.WithCancel(context.Background()) + ctxListenVtxo, ctxCancelListenVtxo := context.WithCancel(context.Background()) cvnt := &covenantlessArkClient{ &arkClient{ - ctxCancelFunc: ctxCancel, - ConfigData: data, - wallet: walletSvc, - sdkRepository: sdkRepository, - explorer: explorerSvc, - client: clientSvc, + ctxListenVtxo: ctxListenVtxo, + ctxCancelListenVtxo: ctxCancelListenVtxo, + ConfigData: data, + wallet: walletSvc, + sdkRepository: sdkRepository, + explorer: explorerSvc, + client: clientSvc, }, } - if err := cvnt.listenToVtxoChan(ctx); err != nil { - return nil, err - } - return cvnt, nil } @@ -164,22 +158,19 @@ func LoadCovenantlessClientWithWallet( return nil, fmt.Errorf("failed to setup explorer: %s", err) } - ctx, ctxCancel := context.WithCancel(context.Background()) + ctxListenVtxo, ctxCancelListenVtxo := context.WithCancel(context.Background()) cvnt := &covenantlessArkClient{ &arkClient{ - ctxCancelFunc: ctxCancel, - ConfigData: data, - wallet: walletSvc, - sdkRepository: sdkRepository, - explorer: explorerSvc, - client: clientSvc, + ctxListenVtxo: ctxListenVtxo, + ctxCancelListenVtxo: ctxCancelListenVtxo, + ConfigData: data, + wallet: walletSvc, + sdkRepository: sdkRepository, + explorer: explorerSvc, + client: clientSvc, }, } - if err := cvnt.listenToVtxoChan(ctx); err != nil { - return nil, err - } - return cvnt, nil } @@ -774,11 +765,42 @@ func (a *covenantlessArkClient) GetTransactionEventChannel() chan domain.Transac return a.sdkRepository.AppDataRepository().TransactionRepository().GetEventChannel() } -func (a *covenantlessArkClient) listenToVtxoChan(ctnx context.Context) error { +func (a *covenantlessArkClient) ListenToVtxoChan() error { + var wg sync.WaitGroup + a.listeningToVtxo = true go func(ctx context.Context) { - ticker := time.NewTicker(1 * time.Second) + ticker := time.NewTicker(2 * time.Second) defer ticker.Stop() + performAction := func() { + wg.Add(1) + defer wg.Done() + // Use a context that won't be canceled prematurely by arkClient ctx + ct := context.Background() + + allSpendableVtxos, allSpentVtxos, err := a.ListVtxos(ct) + if err != nil { + log.Errorf("failed to list vtxos: %s", err) + return + } + + allBoardingTxs := a.getBoardingTxs(ct) + + if err := a.processVtxosAndTxs( + ct, + allSpendableVtxos, + allSpentVtxos, + allBoardingTxs, + ); err != nil { + log.Errorf("failed to process vtxos: %s", err) + return + } + log.Info("processed vtxos") + } + + // initial action + performAction() + for { if !a.sdkInitialized { continue @@ -787,28 +809,13 @@ func (a *covenantlessArkClient) listenToVtxoChan(ctnx context.Context) error { select { case <-ctx.Done(): log.Info("stopping listening to vtxos") + wg.Wait() return case <-ticker.C: - allSpendableVtxos, allSpentVtxos, err := a.ListVtxos(ctx) - if err != nil { - log.Errorf("failed to list vtxos: %s", err) - continue - } - - allBoardingTxs := a.getBoardingTxs(ctx) - - if err := a.processVtxosAndTxs( - ctx, - allSpendableVtxos, - allSpentVtxos, - allBoardingTxs, - ); err != nil { - log.Errorf("failed to process vtxos: %s", err) - continue - } + performAction() } } - }(ctnx) + }(a.ctxListenVtxo) return nil } @@ -823,53 +830,15 @@ func (a *covenantlessArkClient) processVtxosAndTxs( return fmt.Errorf("failed to process txs: %s", err) } - return a.processVtxos(ctx, allSpendableVtxos, allSpentVtxos) -} - -func (a *covenantlessArkClient) processBoardingTxs( - ctx context.Context, - allBoardingTxs []domain.Transaction, -) error { - oldBoardingTxs, err := a.sdkRepository.AppDataRepository(). - TransactionRepository().GetBoardingTxs(ctx) - if err != nil { - return fmt.Errorf("failed to get boarding txs: %s", err) - } - - if len(oldBoardingTxs) == 0 { - if err := a.sdkRepository.AppDataRepository().TransactionRepository(). - InsertTransactions(ctx, allBoardingTxs); err != nil { - return fmt.Errorf("failed to insert boarding txs: %s", err) - } - } else { - newBoardingTxs, updatedOldBoardingTxs := updateBoardingTxsState(allBoardingTxs, oldBoardingTxs) - if err := a.sdkRepository.AppDataRepository().TransactionRepository(). - InsertTransactions(ctx, newBoardingTxs); err != nil { - return fmt.Errorf("failed to insert boarding txs: %s", err) - } - - if err := a.sdkRepository.AppDataRepository().TransactionRepository(). - UpdateTransactions(ctx, updatedOldBoardingTxs); err != nil { - return fmt.Errorf("failed to update boarding txs: %s", err) - } - } - - return nil + return a.processVtxos(ctx, allSpendableVtxos, allSpentVtxos, allBoardingTxs) } func (a *covenantlessArkClient) processVtxos( ctx context.Context, allSpendableVtxos, allSpentVtxos []client.Vtxo, + allBoardingTxs []domain.Transaction, ) error { - spendableVtxosOld, spentVtxosOld, err := a.sdkRepository. - AppDataRepository().VtxoRepository().GetAll(ctx) - if err != nil { - return fmt.Errorf("failed to get vtxos: %s", err) - } - - oldVtxos := append(spendableVtxosOld, spentVtxosOld...) - allTxs, err := vtxosToTxsCovenantless( a.ConfigData.RoundInterval, allSpendableVtxos, @@ -878,15 +847,10 @@ func (a *covenantlessArkClient) processVtxos( if err != nil { return err } - if len(oldVtxos) == 0 { - err = a.insertVtxosAndTransactions( - ctx, - allTxs, - allSpendableVtxos, - allSpentVtxos, - ) - if err != nil { - return fmt.Errorf("failed to process new vtxos: %s", err) + if len(allBoardingTxs) == 0 { + if err := a.sdkRepository.AppDataRepository().TransactionRepository(). + InsertTransactions(ctx, allTxs); err != nil { + return fmt.Errorf("failed to insert txs: %s", err) } } else { oldTxs, err := a.sdkRepository.AppDataRepository().TransactionRepository().GetAll(ctx) @@ -895,41 +859,41 @@ func (a *covenantlessArkClient) processVtxos( } newTxs, err := findNewTxs(oldTxs, allTxs) - err = a.insertVtxosAndTransactions( - ctx, - newTxs, - allSpendableVtxos, - allSpentVtxos, - ) - if err != nil { - return fmt.Errorf("failed to process new vtxos: %s", err) + if err := a.sdkRepository.AppDataRepository().TransactionRepository(). + InsertTransactions(ctx, newTxs); err != nil { + return fmt.Errorf("failed to insert txs: %s", err) } } return nil } -func (a *covenantlessArkClient) insertVtxosAndTransactions( +func (a *covenantlessArkClient) processBoardingTxs( ctx context.Context, - txs []domain.Transaction, - spendableVtxos []client.Vtxo, - spentVtxos []client.Vtxo, + allBoardingTxs []domain.Transaction, ) error { - if err := a.sdkRepository.AppDataRepository().TransactionRepository(). - InsertTransactions(ctx, txs); err != nil { - return err + oldBoardingTxs, err := a.sdkRepository.AppDataRepository(). + TransactionRepository().GetBoardingTxs(ctx) + if err != nil { + return fmt.Errorf("failed to get boarding txs: %s", err) } - domainVtxos := convertVtxosToDomainVtxos(spendableVtxos, false) - domainVtxos = append(domainVtxos, convertVtxosToDomainVtxos(spentVtxos, true)...) - - if len(domainVtxos) > 0 { - if err := a.sdkRepository.AppDataRepository().VtxoRepository().DeleteAll(ctx); err != nil { - return err + if len(oldBoardingTxs) == 0 { + if err := a.sdkRepository.AppDataRepository().TransactionRepository(). + InsertTransactions(ctx, allBoardingTxs); err != nil { + return fmt.Errorf("failed to insert boarding txs: %s", err) + } + } else { + newBoardingTxs, updatedOldBoardingTxs := updateBoardingTxsState(allBoardingTxs, oldBoardingTxs) + if err := a.sdkRepository.AppDataRepository().TransactionRepository(). + InsertTransactions(ctx, newBoardingTxs); err != nil { + return fmt.Errorf("failed to insert boarding txs: %s", err) } - return a.sdkRepository.AppDataRepository().VtxoRepository(). - InsertVtxos(ctx, domainVtxos) + if err := a.sdkRepository.AppDataRepository().TransactionRepository(). + UpdateTransactions(ctx, updatedOldBoardingTxs); err != nil { + return fmt.Errorf("failed to update boarding txs: %s", err) + } } return nil @@ -1977,21 +1941,31 @@ func (a *covenantlessArkClient) getOffchainBalance( return balance, amountByExpiration, nil } -func (a *covenantlessArkClient) getAllBoardingUtxos(ctx context.Context) ([]explorer.Utxo, error) { +func (a *covenantlessArkClient) getAllBoardingUtxos( + ctx context.Context, +) ([]explorer.Utxo, map[string]struct{}, error) { _, boardingAddrs, _, err := a.wallet.GetAddresses(ctx) if err != nil { - return nil, err + return nil, nil, err } utxos := []explorer.Utxo{} + ignoreVtxos := make(map[string]struct{}, 0) for _, addr := range boardingAddrs { txs, err := a.explorer.GetTxs(addr) if err != nil { - continue + return nil, nil, err } for _, tx := range txs { for i, vout := range tx.Vout { if vout.Address == addr { + spentStatuses, err := a.explorer.GetTxOutspends(tx.Txid) + if err != nil { + return nil, nil, err + } + if s := spentStatuses[i]; s.Spent { + ignoreVtxos[s.SpentBy] = struct{}{} + } createdAt := time.Time{} if tx.Status.Confirmed { createdAt = time.Unix(tx.Status.Blocktime, 0) @@ -2007,7 +1981,7 @@ func (a *covenantlessArkClient) getAllBoardingUtxos(ctx context.Context) ([]expl } } - return utxos, nil + return utxos, ignoreVtxos, nil } func (a *covenantlessArkClient) getClaimableBoardingUtxos(ctx context.Context) ([]explorer.Utxo, error) { @@ -2196,10 +2170,19 @@ func (a *covenantlessArkClient) offchainAddressToDefaultVtxoDescriptor(addr stri return vtxoScript.ToDescriptor(), nil } -func (a *covenantlessArkClient) getBoardingTxs(ctx context.Context) (transactions []domain.Transaction) { +// getBoardingTxs builds the boarding tx history from onchain utxos: +// - unspent utxo => pending boarding tx +// - spent utxo => claimed boarding tx +// +// The tx spending an onchain utxo is an ark round, therefore an indexed list +// of round txids is returned to specify the vtxos to be ignored to build the +// offchain tx history and prevent duplicates. +func (a *covenantlessArkClient) getBoardingTxs( + ctx context.Context, +) ([]domain.Transaction, map[string]struct{}, error) { utxos, err := a.getClaimableBoardingUtxos(ctx) if err != nil { - return nil + return nil, nil, err } isPending := make(map[string]bool) @@ -2207,90 +2190,132 @@ func (a *covenantlessArkClient) getBoardingTxs(ctx context.Context) (transaction isPending[u.Txid] = true } - allUtxos, err := a.getAllBoardingUtxos(ctx) + allUtxos, ignoreVtxos, err := a.getAllBoardingUtxos(ctx) if err != nil { - return nil + return nil, nil, err } + unconfirmedTxs := make([]domain.Transaction, 0) + confirmedTxs := make([]domain.Transaction, 0) for _, u := range allUtxos { pending := false if isPending[u.Txid] { pending = true } - transactions = append(transactions, domain.Transaction{ + + tx := domain.Transaction{ BoardingTxid: u.Txid, Amount: u.Amount, Type: domain.TxReceived, IsPending: pending, CreatedAt: u.CreatedAt, - }) - } - return -} + } -func findVtxosBySpentBy(allVtxos []client.Vtxo, txid string) (vtxos []client.Vtxo) { - for _, v := range allVtxos { - if v.SpentBy == txid { - vtxos = append(vtxos, v) + emptyTime := time.Time{} + if u.CreatedAt == emptyTime { + unconfirmedTxs = append(unconfirmedTxs, tx) + continue } + confirmedTxs = append(confirmedTxs, tx) } - return + + txs := append(unconfirmedTxs, confirmedTxs...) + return txs, ignoreVtxos, nil } func vtxosToTxsCovenantless( - roundLifetime int64, spendable, spent []client.Vtxo, + roundLifetime int64, spendable, spent []client.Vtxo, ignoreVtxos map[string]struct{}, ) ([]domain.Transaction, error) { - txs := make([]domain.Transaction, 0) - for _, v := range append(spendable, spent...) { - // get vtxo amount - amount := int(v.Amount) - // ignore not pending - if !v.Pending { + transactions := make([]domain.Transaction, 0) + + indexedTxs := make(map[string]domain.Transaction) + for _, v := range spent { + // If the vtxo was pending and is spent => it's been claimed. + if v.Pending { + transactions = append(transactions, domain.Transaction{ + RedeemTxid: v.Txid, + Amount: v.Amount, + Type: domain.TxReceived, + IsPending: false, + CreatedAt: getCreatedAtFromExpiry(roundLifetime, *v.ExpiresAt), + }) + // Delete any duplicate in the indexed list. + delete(indexedTxs, v.SpentBy) + // Ignore the spendable vtxo created by the claim. + ignoreVtxos[v.SpentBy] = struct{}{} continue } - // find other spent vtxos that spent this one - relatedVtxos := findVtxosBySpentBy(spent, v.Txid) - for _, r := range relatedVtxos { - if r.Amount < math.MaxInt64 { - rAmount := int(r.Amount) - amount -= rAmount + + // If this vtxo spent another one => subtract the amount to find the sent amount. + if tx, ok := indexedTxs[v.Txid]; ok { + tx.Amount -= v.Amount + if v.RedeemTx == "" { + tx.RedeemTxid = "" + } else { + tx.RoundTxid = "" } + indexedTxs[v.Txid] = tx + } + + // Add a transaction to the indexed list if not existing, it will be deleted if it's a duplicate. + tx, ok := indexedTxs[v.SpentBy] + if !ok { + indexedTxs[v.SpentBy] = domain.Transaction{ + RedeemTxid: v.SpentBy, + RoundTxid: v.SpentBy, + Amount: v.Amount, + Type: domain.TxSent, + IsPending: false, + CreatedAt: getCreatedAtFromExpiry(roundLifetime, *v.ExpiresAt), + } + continue } - // what kind of tx was this? send or receive? - txType := domain.TxReceived - if amount < 0 { - txType = domain.TxSent + + // Otherwise add the amount of this vtxo to the one of the tx in the indexed list. + tx.Amount += v.Amount + indexedTxs[v.SpentBy] = tx + } + + for _, v := range spendable { + _, ok1 := ignoreVtxos[v.Txid] + _, ok2 := ignoreVtxos[v.RoundTxid] + if ok1 || ok2 { + continue + } + txid := v.RoundTxid + if txid == "" { + txid = v.Txid } - // get redeem txid - redeemTxid := "" - if len(v.RedeemTx) > 0 { - txid, err := getRedeemTxidCovenantless(v.RedeemTx) - if err != nil { - return nil, err + tx, ok := indexedTxs[txid] + if !ok { + redeemTxid := "" + if v.RoundTxid == "" { + redeemTxid = v.Txid } - redeemTxid = txid - } - // add transaction - txs = append(txs, domain.Transaction{ - RoundTxid: v.RoundTxid, - RedeemTxid: redeemTxid, - Amount: uint64(math.Abs(float64(amount))), - Type: txType, - IsPending: v.Pending && len(v.SpentBy) == 0, - IsPendingChange: v.PendingChange, - CreatedAt: getCreatedAtFromExpiry(roundLifetime, *v.ExpiresAt), - }) - } + transactions = append(transactions, domain.Transaction{ + RedeemTxid: redeemTxid, + RoundTxid: v.RoundTxid, + Amount: v.Amount, + Type: domain.TxReceived, + IsPending: v.Pending, + CreatedAt: getCreatedAtFromExpiry(roundLifetime, *v.ExpiresAt), + }) + continue + } - return txs, nil -} + tx.Amount -= v.Amount + if v.RedeemTx == "" { + tx.RedeemTxid = "" + } else { + tx.RoundTxid = "" + } + indexedTxs[txid] = tx + } -func getRedeemTxidCovenantless(redeemTx string) (string, error) { - redeemPtx, err := psbt.NewFromRawBytes(strings.NewReader(redeemTx), true) - if err != nil { - return "", fmt.Errorf("failed to parse redeem tx: %s", err) + for _, tx := range indexedTxs { + transactions = append(transactions, tx) } - return redeemPtx.UnsignedTx.TxID(), nil + return transactions, nil } diff --git a/pkg/client-sdk/store/badger/app_data_repository.go b/pkg/client-sdk/store/badger/app_data_repository.go index 49b28a207..d3082eade 100644 --- a/pkg/client-sdk/store/badger/app_data_repository.go +++ b/pkg/client-sdk/store/badger/app_data_repository.go @@ -6,16 +6,13 @@ import ( type appDataRepository struct { transactionRepository domain.TransactionRepository - vtxoRepository domain.VtxoRepository } func NewAppDataRepository( transactionRepository domain.TransactionRepository, - vtxoRepository domain.VtxoRepository, ) domain.AppDataRepository { return &appDataRepository{ transactionRepository: transactionRepository, - vtxoRepository: vtxoRepository, } } @@ -23,14 +20,10 @@ func (a *appDataRepository) TransactionRepository() domain.TransactionRepository return a.transactionRepository } -func (a *appDataRepository) VtxoRepository() domain.VtxoRepository { - return a.vtxoRepository -} - func (a *appDataRepository) Stop() error { if err := a.transactionRepository.Stop(); err != nil { return err } - return a.vtxoRepository.Stop() + return nil } diff --git a/pkg/client-sdk/store/badger/vtxo_repository.go b/pkg/client-sdk/store/badger/vtxo_repository.go deleted file mode 100644 index 452b4296a..000000000 --- a/pkg/client-sdk/store/badger/vtxo_repository.go +++ /dev/null @@ -1,68 +0,0 @@ -package badgerstore - -import ( - "context" - "fmt" - "path/filepath" - - "github.com/ark-network/ark/pkg/client-sdk/store/domain" - "github.com/dgraph-io/badger/v4" - "github.com/timshannon/badgerhold/v4" -) - -const ( - vtxoStoreDir = "vtxos" -) - -type vtxoRepository struct { - db *badgerhold.Store -} - -func NewVtxoRepository(dir string, logger badger.Logger) (domain.VtxoRepository, error) { - badgerDb, err := CreateDB(filepath.Join(dir, vtxoStoreDir), logger) - if err != nil { - return nil, fmt.Errorf("failed to open round events store: %s", err) - } - return &vtxoRepository{ - db: badgerDb, - }, nil -} - -func (v *vtxoRepository) InsertVtxos(ctx context.Context, vtxos []domain.Vtxo) error { - for _, vtxo := range vtxos { - if err := v.db.Insert(vtxo.Key(), &vtxo); err != nil { - return err - } - } - return nil -} - -func (v *vtxoRepository) GetAll( - ctx context.Context, -) (spendable []domain.Vtxo, spent []domain.Vtxo, err error) { - var allVtxos []domain.Vtxo - err = v.db.Find(&allVtxos, nil) - if err != nil { - return nil, nil, err - } - - for _, vtxo := range allVtxos { - if vtxo.Spent { - spent = append(spent, vtxo) - } else { - spendable = append(spendable, vtxo) - } - } - return -} - -func (v *vtxoRepository) DeleteAll(ctx context.Context) error { - if err := v.db.DeleteMatching(&domain.Vtxo{}, nil); err != nil { - return fmt.Errorf("failed to delete all vtxos: %w", err) - } - return nil -} - -func (v *vtxoRepository) Stop() error { - return v.db.Close() -} diff --git a/pkg/client-sdk/store/domain/repository.go b/pkg/client-sdk/store/domain/repository.go index 432685389..094fcc742 100644 --- a/pkg/client-sdk/store/domain/repository.go +++ b/pkg/client-sdk/store/domain/repository.go @@ -9,7 +9,6 @@ type SdkRepository interface { type AppDataRepository interface { TransactionRepository() TransactionRepository - VtxoRepository() VtxoRepository Stop() error } @@ -30,10 +29,3 @@ type TransactionRepository interface { GetBoardingTxs(ctx context.Context) ([]Transaction, error) Stop() error } - -type VtxoRepository interface { - InsertVtxos(ctx context.Context, vtxos []Vtxo) error - GetAll(ctx context.Context) (spendable []Vtxo, spent []Vtxo, err error) - DeleteAll(ctx context.Context) error - Stop() error -} diff --git a/pkg/client-sdk/store/service.go b/pkg/client-sdk/store/service.go index 63c015045..e7d408118 100644 --- a/pkg/client-sdk/store/service.go +++ b/pkg/client-sdk/store/service.go @@ -32,7 +32,6 @@ func NewService(storeConfig Config) (domain.SdkRepository, error) { configRepository domain.ConfigRepository appDataRepository domain.AppDataRepository transactionRepository domain.TransactionRepository - vtxoRepository domain.VtxoRepository err error dir = storeConfig.BaseDir @@ -62,17 +61,8 @@ func NewService(storeConfig Config) (domain.SdkRepository, error) { return nil, err } - vtxoRepository, err = badgerstore.NewVtxoRepository( - dir, - badgerLogger, - ) - if err != nil { - return nil, err - } - appDataRepository = badgerstore.NewAppDataRepository( transactionRepository, - vtxoRepository, ) } From 79d97418fbec7f433bfd460edd4a06a7d2380b25 Mon Sep 17 00:00:00 2001 From: sekulicd Date: Thu, 26 Sep 2024 17:01:36 +0200 Subject: [PATCH 15/15] merge and fixes --- client/main.go | 5 +- pkg/client-sdk/ark_sdk.go | 1 - pkg/client-sdk/client_test.go | 115 +------------------- pkg/client-sdk/covenant_client.go | 5 +- pkg/client-sdk/covenant_client_test.go | 90 --------------- pkg/client-sdk/covenantless_client.go | 48 ++++---- pkg/client-sdk/types.go | 20 ---- pkg/client-sdk/wasm/browser/config_store.go | 13 --- 8 files changed, 38 insertions(+), 259 deletions(-) diff --git a/client/main.go b/client/main.go index e30e290b2..0b5618590 100644 --- a/client/main.go +++ b/client/main.go @@ -7,17 +7,14 @@ import ( "errors" "fmt" "os" + "strings" "syscall" "github.com/ark-network/ark/common" arksdk "github.com/ark-network/ark/pkg/client-sdk" "github.com/ark-network/ark/pkg/client-sdk/store" -<<<<<<< HEAD "github.com/ark-network/ark/pkg/client-sdk/store/domain" -======= - filestore "github.com/ark-network/ark/pkg/client-sdk/store/file" "github.com/btcsuite/btcd/btcutil/psbt" ->>>>>>> master "github.com/urfave/cli/v2" "golang.org/x/term" ) diff --git a/pkg/client-sdk/ark_sdk.go b/pkg/client-sdk/ark_sdk.go index 2281a4711..2519f1e2d 100644 --- a/pkg/client-sdk/ark_sdk.go +++ b/pkg/client-sdk/ark_sdk.go @@ -31,7 +31,6 @@ type ArkClient interface { GetTransactionHistory(ctx context.Context) ([]domain.Transaction, error) GetTransactionEventChannel() chan domain.Transaction Close() error - ListenToVtxoChan() error } type Receiver interface { diff --git a/pkg/client-sdk/client_test.go b/pkg/client-sdk/client_test.go index c6d038ccb..296c5512f 100644 --- a/pkg/client-sdk/client_test.go +++ b/pkg/client-sdk/client_test.go @@ -19,7 +19,6 @@ func TestVtxosToTxsCovenant(t *testing.T) { want []domain.Transaction }{ { -<<<<<<< HEAD name: "Alice Sends to Bob", fixture: aliceToBobCovenant, want: []domain.Transaction{ @@ -36,11 +35,6 @@ func TestVtxosToTxsCovenant(t *testing.T) { IsPending: false, }, }, -======= - name: "Alice Before Sending Async", - fixture: aliceBeforeSendingAsync, - want: []Transaction{}, ->>>>>>> master }, } @@ -67,7 +61,7 @@ func TestVtxosToTxsCovenant(t *testing.T) { } } -func TestVtxosToTxsCovenantless(t *testing.T) { +func TestVtxosToTxs(t *testing.T) { tests := []struct { name string fixture string @@ -76,16 +70,7 @@ func TestVtxosToTxsCovenantless(t *testing.T) { { name: "Alice Before Sending Async", fixture: aliceBeforeSendingAsync, - want: []domain.Transaction{ - { -<<<<<<< HEAD - RoundTxid: "377fa2fbd27c82bdbc095478384c88b6c75432c0ef464189e49c965194446cdf", - Amount: 20000, - Type: domain.TxReceived, - IsPending: false, - CreatedAt: time.Unix(1726054898, 0), - }, - }, + want: []domain.Transaction{}, }, { name: "Alice After Sending Async", @@ -95,13 +80,7 @@ func TestVtxosToTxsCovenantless(t *testing.T) { RedeemTxid: "94fa598302f17f00c8881e742ec0ce2f8c8d16f3d54fe6ba0fb7d13a493d84ad", Amount: 1000, Type: domain.TxSent, - IsPending: true, -======= - RedeemTxid: "94fa598302f17f00c8881e742ec0ce2f8c8d16f3d54fe6ba0fb7d13a493d84ad", - Amount: 1000, - Type: TxSent, IsPending: false, ->>>>>>> master CreatedAt: time.Unix(1726054898, 0), }, }, @@ -109,29 +88,14 @@ func TestVtxosToTxsCovenantless(t *testing.T) { { name: "Bob Before Claiming Async", fixture: bobBeforeClaimingAsync, -<<<<<<< HEAD want: []domain.Transaction{ { RedeemTxid: "94fa598302f17f00c8881e742ec0ce2f8c8d16f3d54fe6ba0fb7d13a493d84ad", Amount: 1000, Type: domain.TxReceived, -======= - want: []Transaction{ - { - RedeemTxid: "94fa598302f17f00c8881e742ec0ce2f8c8d16f3d54fe6ba0fb7d13a493d84ad", - Amount: 1000, - Type: TxReceived, IsPending: true, CreatedAt: time.Unix(1726054898, 0), }, - { - RedeemTxid: "766fc46ba5c2da41cd4c4bc0566e0f4e0f24c184c41acd3bead5cd7b11120367", - Amount: 2000, - Type: TxReceived, ->>>>>>> master - IsPending: true, - CreatedAt: time.Unix(1726486359, 0), - }, { RedeemTxid: "766fc46ba5c2da41cd4c4bc0566e0f4e0f24c184c41acd3bead5cd7b11120367", Amount: 2000, @@ -146,28 +110,16 @@ func TestVtxosToTxsCovenantless(t *testing.T) { fixture: bobAfterClaimingAsync, want: []domain.Transaction{ { -<<<<<<< HEAD - RedeemTxid: "766fc46ba5c2da41cd4c4bc0566e0f4e0f24c184c41acd3bead5cd7b11120367", - Amount: 2000, - Type: domain.TxReceived, -======= RedeemTxid: "94fa598302f17f00c8881e742ec0ce2f8c8d16f3d54fe6ba0fb7d13a493d84ad", Amount: 1000, - Type: TxReceived, ->>>>>>> master + Type: domain.TxReceived, IsPending: false, CreatedAt: time.Unix(1726054898, 0), }, { -<<<<<<< HEAD - RedeemTxid: "94fa598302f17f00c8881e742ec0ce2f8c8d16f3d54fe6ba0fb7d13a493d84ad", - Amount: 1000, - Type: domain.TxReceived, -======= RedeemTxid: "766fc46ba5c2da41cd4c4bc0566e0f4e0f24c184c41acd3bead5cd7b11120367", Amount: 2000, - Type: TxReceived, ->>>>>>> master + Type: domain.TxReceived, IsPending: false, CreatedAt: time.Unix(1726486359, 0), }, @@ -178,19 +130,11 @@ func TestVtxosToTxsCovenantless(t *testing.T) { fixture: bobAfterSendingAsync, want: []domain.Transaction{ { -<<<<<<< HEAD - RedeemTxid: "23c3a885f0ea05f7bdf83f3bf7f8ac9dc3f791ad292f4e63a6f53fa5e4935ab0", - Amount: 2100, - Type: domain.TxSent, - IsPending: true, - CreatedAt: time.Unix(1726503865, 0), -======= RedeemTxid: "94fa598302f17f00c8881e742ec0ce2f8c8d16f3d54fe6ba0fb7d13a493d84ad", Amount: 1000, - Type: TxReceived, + Type: domain.TxReceived, IsPending: false, CreatedAt: time.Unix(1726054898, 0), ->>>>>>> master }, { RedeemTxid: "766fc46ba5c2da41cd4c4bc0566e0f4e0f24c184c41acd3bead5cd7b11120367", @@ -200,15 +144,9 @@ func TestVtxosToTxsCovenantless(t *testing.T) { CreatedAt: time.Unix(1726486359, 0), }, { -<<<<<<< HEAD - RedeemTxid: "94fa598302f17f00c8881e742ec0ce2f8c8d16f3d54fe6ba0fb7d13a493d84ad", - Amount: 1000, - Type: domain.TxReceived, -======= RedeemTxid: "23c3a885f0ea05f7bdf83f3bf7f8ac9dc3f791ad292f4e63a6f53fa5e4935ab0", Amount: 2100, - Type: TxSent, ->>>>>>> master + Type: domain.TxSent, IsPending: false, CreatedAt: time.Unix(1726503865, 0), }, @@ -218,19 +156,11 @@ func TestVtxosToTxsCovenantless(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { -<<<<<<< HEAD - vtxos, _, err := loadFixtures(tt.fixture) - if err != nil { - t.Fatalf("failed to load fixture: %s", err) - } - got, err := vtxosToTxsCovenantless(30, vtxos.spendable, vtxos.spent) -======= vtxos, ignoreTxs, err := loadFixtures(tt.fixture) if err != nil { t.Fatalf("failed to load fixture: %s", err) } got, err := vtxosToTxsCovenantless(30, vtxos.spendable, vtxos.spent, ignoreTxs) ->>>>>>> master require.NoError(t, err) require.Len(t, got, len(tt.want)) @@ -252,23 +182,9 @@ type vtxos struct { spent []client.Vtxo } -<<<<<<< HEAD -func loadFixtures(jsonStr string) (vtxos, []domain.Transaction, error) { - var data struct { - BoardingTxs []struct { - BoardingTxid string `json:"boardingTxid"` - RoundTxid string `json:"roundTxid"` - Amount uint64 `json:"amount"` - Type domain.TxType `json:"txType"` - Pending bool `json:"pending"` - Claimed bool `json:"claimed"` - CreatedAt string `json:"createdAt"` - } `json:"boardingTxs"` -======= func loadFixtures(jsonStr string) (vtxos, map[string]struct{}, error) { var data struct { IgnoreTxs []string `json:"ignoreTxs"` ->>>>>>> master SpendableVtxos []struct { Outpoint struct { Txid string `json:"txid"` @@ -365,25 +281,6 @@ func loadFixtures(jsonStr string) (vtxos, map[string]struct{}, error) { } } -<<<<<<< HEAD - boardingTxs := make([]domain.Transaction, len(data.BoardingTxs)) - for i, tx := range data.BoardingTxs { - createdAt, err := parseTimestamp(tx.CreatedAt) - if err != nil { - return vtxos{}, nil, err - } - boardingTxs[i] = domain.Transaction{ - BoardingTxid: tx.BoardingTxid, - RoundTxid: tx.RoundTxid, - Amount: tx.Amount, - Type: domain.TxReceived, - IsPending: tx.Pending, - CreatedAt: createdAt, - } - } - -======= ->>>>>>> master vtxos := vtxos{ spendable: spendable, spent: spent, diff --git a/pkg/client-sdk/covenant_client.go b/pkg/client-sdk/covenant_client.go index f4bb9deba..2a2b1af16 100644 --- a/pkg/client-sdk/covenant_client.go +++ b/pkg/client-sdk/covenant_client.go @@ -585,10 +585,11 @@ func (a *covenantArkClient) Claim(ctx context.Context) (string, error) { } func (a *covenantArkClient) GetTransactionEventChannel() chan domain.Transaction { + a.listenToVtxoChan() return a.sdkRepository.AppDataRepository().TransactionRepository().GetEventChannel() } -func (a *covenantArkClient) ListenToVtxoChan() error { +func (a *covenantArkClient) listenToVtxoChan() { a.listeningToVtxo = true var wg sync.WaitGroup @@ -640,8 +641,6 @@ func (a *covenantArkClient) ListenToVtxoChan() error { } } }(a.ctxListenVtxo) - - return nil } func (a *covenantArkClient) processVtxosAndTxs( diff --git a/pkg/client-sdk/covenant_client_test.go b/pkg/client-sdk/covenant_client_test.go index d6b778dc0..5b2294671 100644 --- a/pkg/client-sdk/covenant_client_test.go +++ b/pkg/client-sdk/covenant_client_test.go @@ -1,103 +1,13 @@ package arksdk import ( - "context" - "encoding/hex" "testing" "time" - "github.com/ark-network/ark/pkg/client-sdk/client" - "github.com/ark-network/ark/pkg/client-sdk/store" "github.com/ark-network/ark/pkg/client-sdk/store/domain" - "github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) -func TestCovenantArkClientListenToVtxoChan(t *testing.T) { - var ( - ctx = context.Background() - err error - sdkRepo domain.SdkRepository - txs []domain.Transaction - spendableVtxosOld, spentVtxosOld []domain.Vtxo - ) - - sdkRepo, err = store.NewService(store.Config{ - ConfigStoreType: store.FileStore, - AppDataStoreType: store.Badger, - BaseDir: t.TempDir(), - }) - require.NoError(t, err) - by, err := hex.DecodeString("020000000000000000000000000000000000000000000000000000000000000001") - require.NoError(t, err) - aspPubkey, err := secp256k1.ParsePubKey(by) - - arkC := arkClient{ - ConfigData: &domain.ConfigData{ - AspPubkey: aspPubkey, - RoundInterval: 20, - }, - sdkRepository: sdkRepo, - sdkInitialized: false, - } - - covenantArkClient := &covenantArkClient{ - arkClient: &arkC, - } - - err = covenantArkClient.processVtxosAndTxs(ctx, nil, nil, []domain.Transaction{boardingTx1}) - require.NoError(t, err) - - txs, err = sdkRepo.AppDataRepository().TransactionRepository().GetAll(ctx) - require.NoError(t, err) - require.Len(t, txs, 1) - - spendableVtxosOld, spentVtxosOld, err = sdkRepo.AppDataRepository().VtxoRepository().GetAll(ctx) - require.NoError(t, err) - require.Len(t, spendableVtxosOld, 0) - require.Len(t, spentVtxosOld, 0) - - spendableVtxo := []client.Vtxo{vtxo1} - err = covenantArkClient.processVtxosAndTxs(ctx, spendableVtxo, nil, []domain.Transaction{boardingTx1}) - require.NoError(t, err) - - spentVtxo := []client.Vtxo{vtxo2} - err = covenantArkClient.processVtxosAndTxs(ctx, spendableVtxo, spentVtxo, []domain.Transaction{boardingTx1}) - require.NoError(t, err) -} - -var boardingTx1 = domain.Transaction{ - BoardingTxid: "ecba8b7280ceac8dfcc2c1bb34dd45c8783d09f970e9eb9d30ef436c91c036b6", - Amount: 3000, - Type: domain.TxReceived, - CreatedAt: time.Now(), -} - -var vtxo1 = client.Vtxo{ - Outpoint: client.Outpoint{ - Txid: "b8395c0fbc9cc6e56c172d6d3bebcf030fcc0bb5cf168361d515d62240e01010", - VOut: 0, - }, - Descriptor: "tr(0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,{ and(pk(2763c97adba0ba950b65cbb78f306661a502509251614a1f7805a9facbbc0d22), pk(8ec2a71e3fb19b27b06237ed8453c8eafa7e326d22338446a91d439975c4ed50)), and(older(1024), pk(2763c97adba0ba950b65cbb78f306661a502509251614a1f7805a9facbbc0d22)) })", - Amount: 99999000, - RoundTxid: "2af148e364f9e5e1dfd034ce8ac1a875ab3e341fef43ea29c551992a40150c20", - ExpiresAt: &time.Time{}, - SpentBy: "", -} - -var vtxo2 = client.Vtxo{ - Outpoint: client.Outpoint{ - Txid: "f17c9f987ae8061298a758dbdf7299793bc422244beb2c658e36c91e1f01bb7f", - VOut: 0, - }, - Descriptor: "tr(0250929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0,{ and(pk(2763c97adba0ba950b65cbb78f306661a502509251614a1f7805a9facbbc0d22), pk(8ec2a71e3fb19b27b06237ed8453c8eafa7e326d22338446a91d439975c4ed50)), and(older(1024), pk(2763c97adba0ba950b65cbb78f306661a502509251614a1f7805a9facbbc0d22)) })", - Amount: 100000000, - RoundTxid: "ecba8b7280ceac8dfcc2c1bb34dd45c8783d09f970e9eb9d30ef436c91c036b6", - ExpiresAt: &time.Time{}, - SpentBy: "2af148e364f9e5e1dfd034ce8ac1a875ab3e341fef43ea29c551992a40150c20", -} - func TestUpdateBoardingTxsState(t *testing.T) { now := time.Now() diff --git a/pkg/client-sdk/covenantless_client.go b/pkg/client-sdk/covenantless_client.go index c66b4d55e..96df4b9fb 100644 --- a/pkg/client-sdk/covenantless_client.go +++ b/pkg/client-sdk/covenantless_client.go @@ -762,10 +762,11 @@ func (a *covenantlessArkClient) Claim(ctx context.Context) (string, error) { } func (a *covenantlessArkClient) GetTransactionEventChannel() chan domain.Transaction { + a.listenToVtxoChan() return a.sdkRepository.AppDataRepository().TransactionRepository().GetEventChannel() } -func (a *covenantlessArkClient) ListenToVtxoChan() error { +func (a *covenantlessArkClient) listenToVtxoChan() { var wg sync.WaitGroup a.listeningToVtxo = true go func(ctx context.Context) { @@ -784,13 +785,18 @@ func (a *covenantlessArkClient) ListenToVtxoChan() error { return } - allBoardingTxs, ignoreVtxos, err := a.getBoardingTxs(ct) + boardingTxs, ignoreVtxos, err := a.getBoardingTxs(ct) + if err != nil { + log.Errorf("failed to get boarding txs: %s", err) + return + } if err := a.processVtxosAndTxs( ct, allSpendableVtxos, allSpentVtxos, - allBoardingTxs, + boardingTxs, + ignoreVtxos, ); err != nil { log.Errorf("failed to process vtxos: %s", err) return @@ -816,49 +822,53 @@ func (a *covenantlessArkClient) ListenToVtxoChan() error { } } }(a.ctxListenVtxo) - - return nil } func (a *covenantlessArkClient) processVtxosAndTxs( ctx context.Context, allSpendableVtxos, allSpentVtxos []client.Vtxo, - allBoardingTxs []domain.Transaction, + boardingTxs []domain.Transaction, + ignoreVtxos map[string]struct{}, ) error { - if err := a.processBoardingTxs(ctx, allBoardingTxs); err != nil { + if err := a.processBoardingTxs(ctx, boardingTxs); err != nil { return fmt.Errorf("failed to process txs: %s", err) } - return a.processVtxos(ctx, allSpendableVtxos, allSpentVtxos, allBoardingTxs) + return a.processVtxos(ctx, allSpendableVtxos, allSpentVtxos, ignoreVtxos) } func (a *covenantlessArkClient) processVtxos( ctx context.Context, allSpendableVtxos, allSpentVtxos []client.Vtxo, - allBoardingTxs []domain.Transaction, + ignoreVtxos map[string]struct{}, ) error { allTxs, err := vtxosToTxsCovenantless( a.ConfigData.RoundInterval, allSpendableVtxos, allSpentVtxos, + ignoreVtxos, ) if err != nil { return err } - if len(allBoardingTxs) == 0 { + + oldTxs, err := a.sdkRepository.AppDataRepository().TransactionRepository().GetAll(ctx) + if err != nil { + return fmt.Errorf("failed to get old transactions: %s", err) + } + + if len(oldTxs) == 0 { if err := a.sdkRepository.AppDataRepository().TransactionRepository(). InsertTransactions(ctx, allTxs); err != nil { return fmt.Errorf("failed to insert txs: %s", err) } } else { - oldTxs, err := a.sdkRepository.AppDataRepository().TransactionRepository().GetAll(ctx) + newTxs, err := findNewTxs(oldTxs, allTxs) if err != nil { - return fmt.Errorf("failed to get old transactions: %s", err) + return fmt.Errorf("failed to find new txs: %s", err) } - - newTxs, err := findNewTxs(oldTxs, allTxs) if err := a.sdkRepository.AppDataRepository().TransactionRepository(). InsertTransactions(ctx, newTxs); err != nil { return fmt.Errorf("failed to insert txs: %s", err) @@ -2199,7 +2209,7 @@ func (a *covenantlessArkClient) offchainAddressToDefaultVtxoDescriptor(addr stri // offchain tx history and prevent duplicates. func (a *covenantlessArkClient) getBoardingTxs( ctx context.Context, -) ([]Transaction, map[string]struct{}, error) { +) ([]domain.Transaction, map[string]struct{}, error) { utxos, err := a.getClaimableBoardingUtxos(ctx) if err != nil { return nil, nil, err @@ -2215,18 +2225,18 @@ func (a *covenantlessArkClient) getBoardingTxs( return nil, nil, err } - unconfirmedTxs := make([]Transaction, 0) - confirmedTxs := make([]Transaction, 0) + unconfirmedTxs := make([]domain.Transaction, 0) + confirmedTxs := make([]domain.Transaction, 0) for _, u := range allUtxos { pending := false if isPending[u.Txid] { pending = true } - tx := Transaction{ + tx := domain.Transaction{ BoardingTxid: u.Txid, Amount: u.Amount, - Type: TxReceived, + Type: domain.TxReceived, IsPending: pending, CreatedAt: u.CreatedAt, } diff --git a/pkg/client-sdk/types.go b/pkg/client-sdk/types.go index 0f2a900e5..8954befab 100644 --- a/pkg/client-sdk/types.go +++ b/pkg/client-sdk/types.go @@ -121,23 +121,3 @@ type balanceRes struct { offchainBalanceByExpiration map[int64]uint64 err error } -<<<<<<< HEAD -======= - -const ( - TxSent TxType = "sent" - TxReceived TxType = "received" -) - -type TxType string - -type Transaction struct { - BoardingTxid string - RoundTxid string - RedeemTxid string - Amount uint64 - Type TxType - IsPending bool - CreatedAt time.Time -} ->>>>>>> master diff --git a/pkg/client-sdk/wasm/browser/config_store.go b/pkg/client-sdk/wasm/browser/config_store.go index ac2ae2f01..a28db5f99 100644 --- a/pkg/client-sdk/wasm/browser/config_store.go +++ b/pkg/client-sdk/wasm/browser/config_store.go @@ -93,18 +93,6 @@ func (s *localStorageStore) GetData(ctx context.Context) (*db.ConfigData, error) unilateralExitDelay, _ := strconv.Atoi(s.store.Call("getItem", "unilateral_exit_delay").String()) dust, _ := strconv.Atoi(s.store.Call("getItem", "min_relay_fee").String()) -<<<<<<< HEAD - return &db.ConfigData{ - AspUrl: s.store.Call("getItem", "asp_url").String(), - AspPubkey: aspPubkey, - WalletType: s.store.Call("getItem", "wallet_type").String(), - ClientType: s.store.Call("getItem", "client_type").String(), - Network: network, - RoundLifetime: int64(roundLifetime), - RoundInterval: int64(roundInterval), - UnilateralExitDelay: int64(unilateralExitDelay), - Dust: uint64(dust), -======= return &store.StoreData{ AspUrl: s.store.Call("getItem", "asp_url").String(), AspPubkey: aspPubkey, @@ -118,7 +106,6 @@ func (s *localStorageStore) GetData(ctx context.Context) (*db.ConfigData, error) ExplorerURL: s.store.Call("getItem", "explorer_url").String(), ForfeitAddress: s.store.Call("getItem", "forfeit_address").String(), BoardingDescriptorTemplate: s.store.Call("getItem", "boarding_descriptor_template").String(), ->>>>>>> master }, nil }