From a58d5190a8c5b461847e8d8717904667c1a9b4b1 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Fri, 30 Aug 2024 10:55:22 -0700 Subject: [PATCH 001/113] tables and interfaces for TSS --- .../2024-08-28.0-tss_transactions.sql | 20 +++++++++++++ ...-28.1-tss_transaction_submission_tries.sql | 18 +++++++++++ internal/tss/channel.go | 30 +++++++++++++++++++ 3 files changed, 68 insertions(+) create mode 100644 internal/db/migrations/2024-08-28.0-tss_transactions.sql create mode 100644 internal/db/migrations/2024-08-28.1-tss_transaction_submission_tries.sql create mode 100644 internal/tss/channel.go diff --git a/internal/db/migrations/2024-08-28.0-tss_transactions.sql b/internal/db/migrations/2024-08-28.0-tss_transactions.sql new file mode 100644 index 0000000..6c8595b --- /dev/null +++ b/internal/db/migrations/2024-08-28.0-tss_transactions.sql @@ -0,0 +1,20 @@ +-- +migrate Up +CREATE TABLE tss_transactions ( + transaction_id VARCHAR(400) PRIMARY KEY, + webhook_url VARCHAR(250), + current_status VARCHAR(50), + creation_time TIMESTAMPTZ DEFAULT NOW(), + last_updated_time TIMESTAMPTZ DEFAULT NOW(), + claimed_until TIMESTAMPTZ +); + +CREATE INDEX idx_tx_current_status ON tss_transactions(current_status); +CREATE INDEX idx_claimed_until ON tss_transactions(claimed_until); + +-- +migrate Down +DROP INDEX IF EXISTS idx_tx_current_status; +DROP INDEX IF EXISTS idx_claimed_until; +DROP TABLE tss_transactions + + + diff --git a/internal/db/migrations/2024-08-28.1-tss_transaction_submission_tries.sql b/internal/db/migrations/2024-08-28.1-tss_transaction_submission_tries.sql new file mode 100644 index 0000000..bf344bd --- /dev/null +++ b/internal/db/migrations/2024-08-28.1-tss_transaction_submission_tries.sql @@ -0,0 +1,18 @@ +-- +migrate Up +CREATE TABLE tss_transaction_submission_tries ( + original_transaction_id VARCHAR(400), + try_transaction_xdr VARCHAR(400), + incoming_status VARCHAR(50), + outgoing_status VARCHAR(50), + last_updated TIMESTAMPTZ DEFAULT NOW() +); + +CREATE INDEX idx_original_transaction_id ON tss_transaction_submission_tries(original_transaction_id); +CREATE INDEX idx_try_transaction_xdr ON tss_transaction_submission_tries(try_transaction_xdr); +CREATE INDEX idx_last_updated ON tss_transaction_submission_tries(last_updated); + +-- +migrate Down +DROP INDEX IF EXISTS idx_original_transaction_id; +DROP INDEX IF EXISTS idx_try_transaction_xdr; +DROP INDEX IF EXISTS idx_last_updated; +DROP TABLE tss_transaction_submission_tries \ No newline at end of file diff --git a/internal/tss/channel.go b/internal/tss/channel.go new file mode 100644 index 0000000..a4666f3 --- /dev/null +++ b/internal/tss/channel.go @@ -0,0 +1,30 @@ +package tss + +type RPCIngestTxResponse struct { + // the raw TransactionEnvelope XDR of the transaction corresponding to the sendTransaction call that returned a PENDING status + envelopeXdr string + // the raw TransactionResult XDR of the envelopeXdr + resultXdr string +} + +type RPCSendTxResponse struct { + // the status of an RPC sendTransaction call. Can be one of [PENDING, DUPLICATE, TRY_AGAIN_LATER, ERROR] + status string + /* + the (optional) error code that is derived by deserialzing the errorResultXdr string in the sendTransaction response + list of possible errror codes: https://developers.stellar.org/docs/data/horizon/api-reference/errors/result-codes/transactions + */ + errorCode string +} + +type RPCPayload struct { + // the transaction xdr submitted by the client + transactionId string + rpcSubmitTxResponse RPCSendTxResponse + rpcIngestTxResponse RPCIngestTxResponse +} + +type Channel interface { + send(payload RPCPayload) + receive(payload RPCPayload) +} From 9920f48ea88a6094ff4e5b52708b48da4c87c137 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Fri, 30 Aug 2024 15:06:40 -0700 Subject: [PATCH 002/113] TSS tables and the channel interface --- .../2024-08-28.0-tss_transactions.sql | 5 ++- ...-28.1-tss_transaction_submission_tries.sql | 10 +++--- internal/tss/channel.go | 34 +++++++++++-------- 3 files changed, 29 insertions(+), 20 deletions(-) diff --git a/internal/db/migrations/2024-08-28.0-tss_transactions.sql b/internal/db/migrations/2024-08-28.0-tss_transactions.sql index 6c8595b..f75c8c3 100644 --- a/internal/db/migrations/2024-08-28.0-tss_transactions.sql +++ b/internal/db/migrations/2024-08-28.0-tss_transactions.sql @@ -1,6 +1,7 @@ -- +migrate Up + CREATE TABLE tss_transactions ( - transaction_id VARCHAR(400) PRIMARY KEY, + transaction_xdr VARCHAR(400) PRIMARY KEY, webhook_url VARCHAR(250), current_status VARCHAR(50), creation_time TIMESTAMPTZ DEFAULT NOW(), @@ -12,9 +13,11 @@ CREATE INDEX idx_tx_current_status ON tss_transactions(current_status); CREATE INDEX idx_claimed_until ON tss_transactions(claimed_until); -- +migrate Down + DROP INDEX IF EXISTS idx_tx_current_status; DROP INDEX IF EXISTS idx_claimed_until; DROP TABLE tss_transactions + diff --git a/internal/db/migrations/2024-08-28.1-tss_transaction_submission_tries.sql b/internal/db/migrations/2024-08-28.1-tss_transaction_submission_tries.sql index bf344bd..1a19079 100644 --- a/internal/db/migrations/2024-08-28.1-tss_transaction_submission_tries.sql +++ b/internal/db/migrations/2024-08-28.1-tss_transaction_submission_tries.sql @@ -1,18 +1,20 @@ -- +migrate Up + CREATE TABLE tss_transaction_submission_tries ( - original_transaction_id VARCHAR(400), + original_transaction_xdr VARCHAR(400), try_transaction_xdr VARCHAR(400), incoming_status VARCHAR(50), outgoing_status VARCHAR(50), last_updated TIMESTAMPTZ DEFAULT NOW() ); -CREATE INDEX idx_original_transaction_id ON tss_transaction_submission_tries(original_transaction_id); +CREATE INDEX idx_original_transaction_xdr ON tss_transaction_submission_tries(original_transaction_xdr); CREATE INDEX idx_try_transaction_xdr ON tss_transaction_submission_tries(try_transaction_xdr); CREATE INDEX idx_last_updated ON tss_transaction_submission_tries(last_updated); -- +migrate Down -DROP INDEX IF EXISTS idx_original_transaction_id; + +DROP INDEX IF EXISTS idx_original_transaction_xdr; DROP INDEX IF EXISTS idx_try_transaction_xdr; DROP INDEX IF EXISTS idx_last_updated; -DROP TABLE tss_transaction_submission_tries \ No newline at end of file +DROP TABLE tss_transaction_submission_tries diff --git a/internal/tss/channel.go b/internal/tss/channel.go index a4666f3..0fbc920 100644 --- a/internal/tss/channel.go +++ b/internal/tss/channel.go @@ -1,30 +1,34 @@ package tss type RPCIngestTxResponse struct { - // the raw TransactionEnvelope XDR of the transaction corresponding to the sendTransaction call that returned a PENDING status - envelopeXdr string + // a status that indicated whether this transaction failed or successly made it to the ledger + Status string + // the raw TransactionEnvelope XDR for this transaction + EnvelopeXdr string // the raw TransactionResult XDR of the envelopeXdr - resultXdr string + ResultXdr string + // The unix timestamp of when the transaction was included in the ledger + CreatedAt int64 } type RPCSendTxResponse struct { // the status of an RPC sendTransaction call. Can be one of [PENDING, DUPLICATE, TRY_AGAIN_LATER, ERROR] - status string - /* - the (optional) error code that is derived by deserialzing the errorResultXdr string in the sendTransaction response - list of possible errror codes: https://developers.stellar.org/docs/data/horizon/api-reference/errors/result-codes/transactions - */ - errorCode string + Status string + // the (optional) error code that is derived by deserialzing the errorResultXdr string in the sendTransaction response + // list of possible errror codes: https://developers.stellar.org/docs/data/horizon/api-reference/errors/result-codes/transactions + ErrorCode string } -type RPCPayload struct { +type Payload struct { // the transaction xdr submitted by the client - transactionId string - rpcSubmitTxResponse RPCSendTxResponse - rpcIngestTxResponse RPCIngestTxResponse + TransactionXdr string + // relevant fields in an RPC sendTransaction response + RpcSubmitTxResponse RPCSendTxResponse + // relevant fields in the transaction list inside the RPC getTransactions response + RpcIngestTxResponse RPCIngestTxResponse } type Channel interface { - send(payload RPCPayload) - receive(payload RPCPayload) + send(payload Payload) + receive(payload Payload) } From 3f9f9f0faf11172d633557cbc5f283349be9205f Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Fri, 30 Aug 2024 15:12:00 -0700 Subject: [PATCH 003/113] remove empty lines --- internal/db/migrations/2024-08-28.0-tss_transactions.sql | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/internal/db/migrations/2024-08-28.0-tss_transactions.sql b/internal/db/migrations/2024-08-28.0-tss_transactions.sql index f75c8c3..edd701b 100644 --- a/internal/db/migrations/2024-08-28.0-tss_transactions.sql +++ b/internal/db/migrations/2024-08-28.0-tss_transactions.sql @@ -16,8 +16,4 @@ CREATE INDEX idx_claimed_until ON tss_transactions(claimed_until); DROP INDEX IF EXISTS idx_tx_current_status; DROP INDEX IF EXISTS idx_claimed_until; -DROP TABLE tss_transactions - - - - +DROP TABLE tss_transactions \ No newline at end of file From 373c71a216e8f192e4fcd1e5e6976362a6a5ed1e Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Fri, 30 Aug 2024 15:12:24 -0700 Subject: [PATCH 004/113] update --- internal/db/migrations/2024-08-28.0-tss_transactions.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/db/migrations/2024-08-28.0-tss_transactions.sql b/internal/db/migrations/2024-08-28.0-tss_transactions.sql index edd701b..8264ba6 100644 --- a/internal/db/migrations/2024-08-28.0-tss_transactions.sql +++ b/internal/db/migrations/2024-08-28.0-tss_transactions.sql @@ -16,4 +16,4 @@ CREATE INDEX idx_claimed_until ON tss_transactions(claimed_until); DROP INDEX IF EXISTS idx_tx_current_status; DROP INDEX IF EXISTS idx_claimed_until; -DROP TABLE tss_transactions \ No newline at end of file +DROP TABLE tss_transactions From 2de9898a5fb03d93f7fe20ab1f26cf004dbe0aef Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Fri, 30 Aug 2024 15:16:53 -0700 Subject: [PATCH 005/113] adding semicolons adding semicolons --- internal/db/migrations/2024-08-28.0-tss_transactions.sql | 2 +- .../2024-08-28.1-tss_transaction_submission_tries.sql | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/db/migrations/2024-08-28.0-tss_transactions.sql b/internal/db/migrations/2024-08-28.0-tss_transactions.sql index 8264ba6..24e10cc 100644 --- a/internal/db/migrations/2024-08-28.0-tss_transactions.sql +++ b/internal/db/migrations/2024-08-28.0-tss_transactions.sql @@ -16,4 +16,4 @@ CREATE INDEX idx_claimed_until ON tss_transactions(claimed_until); DROP INDEX IF EXISTS idx_tx_current_status; DROP INDEX IF EXISTS idx_claimed_until; -DROP TABLE tss_transactions +DROP TABLE tss_transactions; diff --git a/internal/db/migrations/2024-08-28.1-tss_transaction_submission_tries.sql b/internal/db/migrations/2024-08-28.1-tss_transaction_submission_tries.sql index 1a19079..8f332c6 100644 --- a/internal/db/migrations/2024-08-28.1-tss_transaction_submission_tries.sql +++ b/internal/db/migrations/2024-08-28.1-tss_transaction_submission_tries.sql @@ -17,4 +17,4 @@ CREATE INDEX idx_last_updated ON tss_transaction_submission_tries(last_updated); DROP INDEX IF EXISTS idx_original_transaction_xdr; DROP INDEX IF EXISTS idx_try_transaction_xdr; DROP INDEX IF EXISTS idx_last_updated; -DROP TABLE tss_transaction_submission_tries +DROP TABLE tss_transaction_submission_tries; From c0f9d3269c7e681c6d37da805bcca5441d92d17d Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Fri, 30 Aug 2024 15:18:27 -0700 Subject: [PATCH 006/113] moving all migrations to one file --- .../2024-08-28.0-tss_transactions.sql | 17 ++++++++++++++++ ...-28.1-tss_transaction_submission_tries.sql | 20 ------------------- 2 files changed, 17 insertions(+), 20 deletions(-) delete mode 100644 internal/db/migrations/2024-08-28.1-tss_transaction_submission_tries.sql diff --git a/internal/db/migrations/2024-08-28.0-tss_transactions.sql b/internal/db/migrations/2024-08-28.0-tss_transactions.sql index 24e10cc..6c4b72a 100644 --- a/internal/db/migrations/2024-08-28.0-tss_transactions.sql +++ b/internal/db/migrations/2024-08-28.0-tss_transactions.sql @@ -9,11 +9,28 @@ CREATE TABLE tss_transactions ( claimed_until TIMESTAMPTZ ); +CREATE TABLE tss_transaction_submission_tries ( + original_transaction_xdr VARCHAR(400), + try_transaction_xdr VARCHAR(400), + incoming_status VARCHAR(50), + outgoing_status VARCHAR(50), + last_updated TIMESTAMPTZ DEFAULT NOW() +); + CREATE INDEX idx_tx_current_status ON tss_transactions(current_status); CREATE INDEX idx_claimed_until ON tss_transactions(claimed_until); +CREATE INDEX idx_original_transaction_xdr ON tss_transaction_submission_tries(original_transaction_xdr); +CREATE INDEX idx_try_transaction_xdr ON tss_transaction_submission_tries(try_transaction_xdr); +CREATE INDEX idx_last_updated ON tss_transaction_submission_tries(last_updated); + -- +migrate Down DROP INDEX IF EXISTS idx_tx_current_status; DROP INDEX IF EXISTS idx_claimed_until; DROP TABLE tss_transactions; + +DROP INDEX IF EXISTS idx_original_transaction_xdr; +DROP INDEX IF EXISTS idx_try_transaction_xdr; +DROP INDEX IF EXISTS idx_last_updated; +DROP TABLE tss_transaction_submission_tries; diff --git a/internal/db/migrations/2024-08-28.1-tss_transaction_submission_tries.sql b/internal/db/migrations/2024-08-28.1-tss_transaction_submission_tries.sql deleted file mode 100644 index 8f332c6..0000000 --- a/internal/db/migrations/2024-08-28.1-tss_transaction_submission_tries.sql +++ /dev/null @@ -1,20 +0,0 @@ --- +migrate Up - -CREATE TABLE tss_transaction_submission_tries ( - original_transaction_xdr VARCHAR(400), - try_transaction_xdr VARCHAR(400), - incoming_status VARCHAR(50), - outgoing_status VARCHAR(50), - last_updated TIMESTAMPTZ DEFAULT NOW() -); - -CREATE INDEX idx_original_transaction_xdr ON tss_transaction_submission_tries(original_transaction_xdr); -CREATE INDEX idx_try_transaction_xdr ON tss_transaction_submission_tries(try_transaction_xdr); -CREATE INDEX idx_last_updated ON tss_transaction_submission_tries(last_updated); - --- +migrate Down - -DROP INDEX IF EXISTS idx_original_transaction_xdr; -DROP INDEX IF EXISTS idx_try_transaction_xdr; -DROP INDEX IF EXISTS idx_last_updated; -DROP TABLE tss_transaction_submission_tries; From a9cf4e32d70e6b21248fca88aca546a774e89718 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Tue, 3 Sep 2024 01:55:23 -0700 Subject: [PATCH 007/113] make hash primary key instead of xdr --- internal/db/migrations/2024-08-28.0-tss_transactions.sql | 9 +++++---- internal/tss/channel.go | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/internal/db/migrations/2024-08-28.0-tss_transactions.sql b/internal/db/migrations/2024-08-28.0-tss_transactions.sql index 6c4b72a..9ba19f8 100644 --- a/internal/db/migrations/2024-08-28.0-tss_transactions.sql +++ b/internal/db/migrations/2024-08-28.0-tss_transactions.sql @@ -1,7 +1,8 @@ -- +migrate Up CREATE TABLE tss_transactions ( - transaction_xdr VARCHAR(400) PRIMARY KEY, + transaction_hash VARCHAR(70) PRIMARY KEY + transaction_xdr VARCHAR(400), webhook_url VARCHAR(250), current_status VARCHAR(50), creation_time TIMESTAMPTZ DEFAULT NOW(), @@ -10,7 +11,7 @@ CREATE TABLE tss_transactions ( ); CREATE TABLE tss_transaction_submission_tries ( - original_transaction_xdr VARCHAR(400), + original_transaction_hash VARCHAR(70), try_transaction_xdr VARCHAR(400), incoming_status VARCHAR(50), outgoing_status VARCHAR(50), @@ -20,7 +21,7 @@ CREATE TABLE tss_transaction_submission_tries ( CREATE INDEX idx_tx_current_status ON tss_transactions(current_status); CREATE INDEX idx_claimed_until ON tss_transactions(claimed_until); -CREATE INDEX idx_original_transaction_xdr ON tss_transaction_submission_tries(original_transaction_xdr); +CREATE INDEX idx_original_transaction_hash ON tss_transaction_submission_tries(original_transaction_hash); CREATE INDEX idx_try_transaction_xdr ON tss_transaction_submission_tries(try_transaction_xdr); CREATE INDEX idx_last_updated ON tss_transaction_submission_tries(last_updated); @@ -30,7 +31,7 @@ DROP INDEX IF EXISTS idx_tx_current_status; DROP INDEX IF EXISTS idx_claimed_until; DROP TABLE tss_transactions; -DROP INDEX IF EXISTS idx_original_transaction_xdr; +DROP INDEX IF EXISTS idx_original_transaction_hash; DROP INDEX IF EXISTS idx_try_transaction_xdr; DROP INDEX IF EXISTS idx_last_updated; DROP TABLE tss_transaction_submission_tries; diff --git a/internal/tss/channel.go b/internal/tss/channel.go index 0fbc920..33b2374 100644 --- a/internal/tss/channel.go +++ b/internal/tss/channel.go @@ -20,8 +20,8 @@ type RPCSendTxResponse struct { } type Payload struct { - // the transaction xdr submitted by the client - TransactionXdr string + // the hash of the transaction xdr submitted by the client - the id of the transaction submitted by a client + TransactionHash string // relevant fields in an RPC sendTransaction response RpcSubmitTxResponse RPCSendTxResponse // relevant fields in the transaction list inside the RPC getTransactions response From 6fc0dc295fd357d1a0cc04144e27660c4aec29de Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Tue, 3 Sep 2024 08:58:46 -0700 Subject: [PATCH 008/113] missing , --- internal/db/migrations/2024-08-28.0-tss_transactions.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/db/migrations/2024-08-28.0-tss_transactions.sql b/internal/db/migrations/2024-08-28.0-tss_transactions.sql index 9ba19f8..d27e482 100644 --- a/internal/db/migrations/2024-08-28.0-tss_transactions.sql +++ b/internal/db/migrations/2024-08-28.0-tss_transactions.sql @@ -1,7 +1,7 @@ -- +migrate Up CREATE TABLE tss_transactions ( - transaction_hash VARCHAR(70) PRIMARY KEY + transaction_hash VARCHAR(70) PRIMARY KEY, transaction_xdr VARCHAR(400), webhook_url VARCHAR(250), current_status VARCHAR(50), From fb807aafeb9fe21a748c53997cc8c63a53e6924f Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Tue, 3 Sep 2024 10:28:49 -0700 Subject: [PATCH 009/113] remove the index on try_transaction_xdr and add column/index on try_transaction_hash ...also remove outgoing/incoming_status and have one status column --- internal/db/migrations/2024-08-28.0-tss_transactions.sql | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/db/migrations/2024-08-28.0-tss_transactions.sql b/internal/db/migrations/2024-08-28.0-tss_transactions.sql index d27e482..02f2d36 100644 --- a/internal/db/migrations/2024-08-28.0-tss_transactions.sql +++ b/internal/db/migrations/2024-08-28.0-tss_transactions.sql @@ -12,9 +12,9 @@ CREATE TABLE tss_transactions ( CREATE TABLE tss_transaction_submission_tries ( original_transaction_hash VARCHAR(70), + try_transaction_hash VARCHAR(70), try_transaction_xdr VARCHAR(400), - incoming_status VARCHAR(50), - outgoing_status VARCHAR(50), + status VARCHAR(50), last_updated TIMESTAMPTZ DEFAULT NOW() ); @@ -22,7 +22,7 @@ CREATE INDEX idx_tx_current_status ON tss_transactions(current_status); CREATE INDEX idx_claimed_until ON tss_transactions(claimed_until); CREATE INDEX idx_original_transaction_hash ON tss_transaction_submission_tries(original_transaction_hash); -CREATE INDEX idx_try_transaction_xdr ON tss_transaction_submission_tries(try_transaction_xdr); +CREATE INDEX idx_try_transaction_hash ON tss_transaction_submission_tries(try_transaction_hash); CREATE INDEX idx_last_updated ON tss_transaction_submission_tries(last_updated); -- +migrate Down @@ -32,6 +32,6 @@ DROP INDEX IF EXISTS idx_claimed_until; DROP TABLE tss_transactions; DROP INDEX IF EXISTS idx_original_transaction_hash; -DROP INDEX IF EXISTS idx_try_transaction_xdr; +DROP INDEX IF EXISTS idx_try_transaction_hash; DROP INDEX IF EXISTS idx_last_updated; DROP TABLE tss_transaction_submission_tries; From f8c261bf801a90ae8ac3d0a491850fa875f4774e Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Tue, 3 Sep 2024 22:15:45 -0700 Subject: [PATCH 010/113] Squashed commit of the following: commit fb807aafeb9fe21a748c53997cc8c63a53e6924f Author: gouthamp-stellar Date: Tue Sep 3 10:28:49 2024 -0700 remove the index on try_transaction_xdr and add column/index on try_transaction_hash ...also remove outgoing/incoming_status and have one status column commit 6fc0dc295fd357d1a0cc04144e27660c4aec29de Author: gouthamp-stellar Date: Tue Sep 3 08:58:46 2024 -0700 missing , commit a9cf4e32d70e6b21248fca88aca546a774e89718 Author: gouthamp-stellar Date: Tue Sep 3 01:55:23 2024 -0700 make hash primary key instead of xdr commit c0f9d3269c7e681c6d37da805bcca5441d92d17d Author: gouthamp-stellar Date: Fri Aug 30 15:18:27 2024 -0700 moving all migrations to one file commit 2de9898a5fb03d93f7fe20ab1f26cf004dbe0aef Author: gouthamp-stellar Date: Fri Aug 30 15:16:53 2024 -0700 adding semicolons adding semicolons commit 373c71a216e8f192e4fcd1e5e6976362a6a5ed1e Author: gouthamp-stellar Date: Fri Aug 30 15:12:24 2024 -0700 update commit 3f9f9f0faf11172d633557cbc5f283349be9205f Author: gouthamp-stellar Date: Fri Aug 30 15:12:00 2024 -0700 remove empty lines commit 9920f48ea88a6094ff4e5b52708b48da4c87c137 Author: gouthamp-stellar Date: Fri Aug 30 15:06:40 2024 -0700 TSS tables and the channel interface commit a58d5190a8c5b461847e8d8717904667c1a9b4b1 Author: gouthamp-stellar Date: Fri Aug 30 10:55:22 2024 -0700 tables and interfaces for TSS --- .../2024-08-28.0-tss_transactions.sql | 37 +++++++++++++++++++ internal/tss/channel.go | 34 +++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 internal/db/migrations/2024-08-28.0-tss_transactions.sql create mode 100644 internal/tss/channel.go diff --git a/internal/db/migrations/2024-08-28.0-tss_transactions.sql b/internal/db/migrations/2024-08-28.0-tss_transactions.sql new file mode 100644 index 0000000..02f2d36 --- /dev/null +++ b/internal/db/migrations/2024-08-28.0-tss_transactions.sql @@ -0,0 +1,37 @@ +-- +migrate Up + +CREATE TABLE tss_transactions ( + transaction_hash VARCHAR(70) PRIMARY KEY, + transaction_xdr VARCHAR(400), + webhook_url VARCHAR(250), + current_status VARCHAR(50), + creation_time TIMESTAMPTZ DEFAULT NOW(), + last_updated_time TIMESTAMPTZ DEFAULT NOW(), + claimed_until TIMESTAMPTZ +); + +CREATE TABLE tss_transaction_submission_tries ( + original_transaction_hash VARCHAR(70), + try_transaction_hash VARCHAR(70), + try_transaction_xdr VARCHAR(400), + status VARCHAR(50), + last_updated TIMESTAMPTZ DEFAULT NOW() +); + +CREATE INDEX idx_tx_current_status ON tss_transactions(current_status); +CREATE INDEX idx_claimed_until ON tss_transactions(claimed_until); + +CREATE INDEX idx_original_transaction_hash ON tss_transaction_submission_tries(original_transaction_hash); +CREATE INDEX idx_try_transaction_hash ON tss_transaction_submission_tries(try_transaction_hash); +CREATE INDEX idx_last_updated ON tss_transaction_submission_tries(last_updated); + +-- +migrate Down + +DROP INDEX IF EXISTS idx_tx_current_status; +DROP INDEX IF EXISTS idx_claimed_until; +DROP TABLE tss_transactions; + +DROP INDEX IF EXISTS idx_original_transaction_hash; +DROP INDEX IF EXISTS idx_try_transaction_hash; +DROP INDEX IF EXISTS idx_last_updated; +DROP TABLE tss_transaction_submission_tries; diff --git a/internal/tss/channel.go b/internal/tss/channel.go new file mode 100644 index 0000000..33b2374 --- /dev/null +++ b/internal/tss/channel.go @@ -0,0 +1,34 @@ +package tss + +type RPCIngestTxResponse struct { + // a status that indicated whether this transaction failed or successly made it to the ledger + Status string + // the raw TransactionEnvelope XDR for this transaction + EnvelopeXdr string + // the raw TransactionResult XDR of the envelopeXdr + ResultXdr string + // The unix timestamp of when the transaction was included in the ledger + CreatedAt int64 +} + +type RPCSendTxResponse struct { + // the status of an RPC sendTransaction call. Can be one of [PENDING, DUPLICATE, TRY_AGAIN_LATER, ERROR] + Status string + // the (optional) error code that is derived by deserialzing the errorResultXdr string in the sendTransaction response + // list of possible errror codes: https://developers.stellar.org/docs/data/horizon/api-reference/errors/result-codes/transactions + ErrorCode string +} + +type Payload struct { + // the hash of the transaction xdr submitted by the client - the id of the transaction submitted by a client + TransactionHash string + // relevant fields in an RPC sendTransaction response + RpcSubmitTxResponse RPCSendTxResponse + // relevant fields in the transaction list inside the RPC getTransactions response + RpcIngestTxResponse RPCIngestTxResponse +} + +type Channel interface { + send(payload Payload) + receive(payload Payload) +} From d774f2a41d66ed32df9a8ec80376d956a497d4e2 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Thu, 5 Sep 2024 16:48:29 -0700 Subject: [PATCH 011/113] commit #2 --- internal/tss/services/transaction_service.go | 176 ++++++++++++++++++ .../tss/services/transaction_service_test.go | 62 ++++++ 2 files changed, 238 insertions(+) create mode 100644 internal/tss/services/transaction_service.go create mode 100644 internal/tss/services/transaction_service_test.go diff --git a/internal/tss/services/transaction_service.go b/internal/tss/services/transaction_service.go new file mode 100644 index 0000000..beadd31 --- /dev/null +++ b/internal/tss/services/transaction_service.go @@ -0,0 +1,176 @@ +package tss_services + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "log" + "net/http" + + "github.com/stellar/go/clients/horizonclient" + "github.com/stellar/go/txnbuild" + "github.com/stellar/go/xdr" + "github.com/stellar/wallet-backend/internal/signing" + "github.com/stellar/wallet-backend/internal/tss" +) + +type transactionService struct { + DistributionAccountSignatureClient signing.SignatureClient + ChannelAccountSignatureClient signing.SignatureClient + HorizonClient horizonclient.ClientInterface + RpcUrl string + BaseFee int64 +} + +type TransactionServiceOptions struct { + DistributionAccountSignatureClient signing.SignatureClient + ChannelAccountSignatureClient signing.SignatureClient + HorizonClient horizonclient.ClientInterface + RpcUrl string + BaseFee int64 +} + +func (o *TransactionServiceOptions) Validate() error { + if o.DistributionAccountSignatureClient == nil { + return fmt.Errorf("distribution account signature client cannot be nil") + } + + if o.ChannelAccountSignatureClient == nil { + return fmt.Errorf("channel account signature client cannot be nil") + } + + if o.HorizonClient == nil { + return fmt.Errorf("horizon client cannot be nil") + } + + if o.RpcUrl == "" { + return fmt.Errorf("rpc url cannot be empty") + } + + if o.BaseFee < int64(txnbuild.MinBaseFee) { + return fmt.Errorf("base fee is lower than the minimum network fee") + } + return nil +} + +func (t *transactionService) SignAndBuildNewTransaction(ctx context.Context, origTxXdr string) (*txnbuild.FeeBumpTransaction, error) { + genericTx, err := txnbuild.TransactionFromXDR(origTxXdr) + if err != nil { + return nil, fmt.Errorf("deserializing the transaction xdr: %w", err) + } + originalTx, txEmpty := genericTx.Transaction() + if txEmpty { + return nil, fmt.Errorf("empty transaction: %w", err) + } + channelAccountPublicKey, err := t.ChannelAccountSignatureClient.GetAccountPublicKey(ctx) + if err != nil { + return nil, fmt.Errorf("getting channel account public key: %w", err) + } + channelAccount, err := t.HorizonClient.AccountDetail(horizonclient.AccountRequest{AccountID: channelAccountPublicKey}) + if err != nil { + return nil, fmt.Errorf("getting channel account details: %w", err) + } + tx, err := txnbuild.NewTransaction( + txnbuild.TransactionParams{ + SourceAccount: &channelAccount, + Operations: originalTx.Operations(), + BaseFee: int64(t.BaseFee), + Preconditions: txnbuild.Preconditions{ + TimeBounds: txnbuild.NewTimeout(10), + }, + IncrementSequenceNum: true, + }, + ) + if err != nil { + return nil, fmt.Errorf("building transaction: %w", err) + } + tx, err = t.ChannelAccountSignatureClient.SignStellarTransaction(ctx, tx, channelAccountPublicKey) + if err != nil { + return nil, fmt.Errorf("signing transaction with channel account: %w", err) + } + // wrap the transaction in a fee bump tx, signed by the distribution account + distributionAccountPublicKey, err := t.DistributionAccountSignatureClient.GetAccountPublicKey(ctx) + if err != nil { + return nil, fmt.Errorf("getting distribution account public key: %w", err) + } + feeBumpTx, err := txnbuild.NewFeeBumpTransaction( + txnbuild.FeeBumpTransactionParams{ + Inner: tx, + FeeAccount: distributionAccountPublicKey, + BaseFee: int64(t.BaseFee), + }, + ) + if err != nil { + return nil, fmt.Errorf("building fee-bump transaction %w", err) + } + feeBumpTx, err = t.DistributionAccountSignatureClient.SignStellarFeeBumpTransaction(ctx, feeBumpTx) + if err != nil { + return nil, fmt.Errorf("signing the fee bump transaction with distribution account: %w", err) + } + return feeBumpTx, nil +} + +func (t *transactionService) SendTransaction(transactionXdr string) (tss.RPCSendTxResponse, error) { + payload := map[string]interface{}{ + "jsonrpc": "2.0", + "id": 1, + "method": "sendTransaction", + "params": map[string]string{ + "transaction": transactionXdr, + }, + } + jsonData, _ := json.Marshal(payload) + + resp, err := http.Post(t.RpcUrl, "application/json", bytes.NewBuffer(jsonData)) + if err != nil { + return tss.RPCSendTxResponse{}, fmt.Errorf("sending POST request to rpc: %v", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return tss.RPCSendTxResponse{}, fmt.Errorf("reading rpc response: %v", err) + } + var res map[string]interface{} + err = json.Unmarshal(body, &res) + if err != nil { + return tss.RPCSendTxResponse{}, fmt.Errorf("parsing rpc response JSON: %v", err) + } + + sendTxResponse := tss.RPCSendTxResponse{} + if result, ok := res["result"].(map[string]interface{}); ok { + if val, exists := result["errorResultXdr"].(string); exists { + errorResult := xdr.TransactionResult{} + errorResult.UnmarshalBinary([]byte(val)) + sendTxResponse.ErrorCode = errorResult.Result.Code.String() + } + if val, exists := result["status"].(string); exists { + sendTxResponse.Status = val + } + } + fmt.Println(sendTxResponse) + prettyResponse, err := json.MarshalIndent(res, "", " ") + if err != nil { + log.Fatalf("Error formatting rpc JSON response: %v", err) + } + fmt.Println(string(prettyResponse)) + return sendTxResponse, nil +} + +func NewTransactionService(opts TransactionServiceOptions) (*transactionService, error) { + /* + if err := opts.Validate(); err != nil { + return nil, err + } + */ + + return &transactionService{ + DistributionAccountSignatureClient: opts.DistributionAccountSignatureClient, + ChannelAccountSignatureClient: opts.ChannelAccountSignatureClient, + HorizonClient: opts.HorizonClient, + RpcUrl: opts.RpcUrl, + BaseFee: opts.BaseFee, + }, nil +} diff --git a/internal/tss/services/transaction_service_test.go b/internal/tss/services/transaction_service_test.go new file mode 100644 index 0000000..53b3ead --- /dev/null +++ b/internal/tss/services/transaction_service_test.go @@ -0,0 +1,62 @@ +package tss_services + +import ( + "fmt" + "testing" + + "github.com/stellar/go/keypair" + "github.com/stellar/go/txnbuild" + "github.com/stretchr/testify/assert" +) + +func TestThis(t *testing.T) { + + tx := buildTestTransaction() + + tsStr, _ := tx.Base64() + oTx, err := txnbuild.TransactionFromXDR(tsStr) + e := false + if err != nil { + e = true + } + assert.False(t, e) + tt, _ := oTx.Transaction() + fmt.Println("Base fee boo: ", tt.BaseFee()) +} + +func TestSendTransaction(t *testing.T) { + txService, _ := NewTransactionService(TransactionServiceOptions{ + DistributionAccountSignatureClient: nil, + ChannelAccountSignatureClient: nil, + HorizonClient: nil, + RpcUrl: "http://localhost:8000/soroban/rpc", + BaseFee: 114, + }) + txStr, _ := buildTestTransaction().Base64() + txService.SendTransaction(txStr) + +} + +func buildTestTransaction() *txnbuild.Transaction { + accountToSponsor := keypair.MustRandom() + + tx, _ := txnbuild.NewTransaction(txnbuild.TransactionParams{ + SourceAccount: &txnbuild.SimpleAccount{ + AccountID: accountToSponsor.Address(), + Sequence: 124, + }, + IncrementSequenceNum: true, + Operations: []txnbuild.Operation{ + &txnbuild.Payment{ + Destination: keypair.MustRandom().Address(), + Amount: "14", + Asset: txnbuild.NativeAsset{}, + }, + }, + //BaseFee: txnbuild.MinBaseFee, + BaseFee: 104, + Preconditions: txnbuild.Preconditions{TimeBounds: txnbuild.NewTimeout(10)}, + }) + return tx + +} From 6b04d62f28e80b1933d2025e6ca4d5ac932429d7 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Thu, 5 Sep 2024 16:49:39 -0700 Subject: [PATCH 012/113] changing from RpcIngestTxResponse -> RpcGetIngestTxResponse --- internal/tss/channel.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/tss/channel.go b/internal/tss/channel.go index 33b2374..2a9096c 100644 --- a/internal/tss/channel.go +++ b/internal/tss/channel.go @@ -1,6 +1,6 @@ package tss -type RPCIngestTxResponse struct { +type RPCGetIngestTxResponse struct { // a status that indicated whether this transaction failed or successly made it to the ledger Status string // the raw TransactionEnvelope XDR for this transaction @@ -25,7 +25,7 @@ type Payload struct { // relevant fields in an RPC sendTransaction response RpcSubmitTxResponse RPCSendTxResponse // relevant fields in the transaction list inside the RPC getTransactions response - RpcIngestTxResponse RPCIngestTxResponse + RpcGetIngestTxResponse RPCGetIngestTxResponse } type Channel interface { From b441a31181d034e622a41b44d31537244779a693 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Fri, 6 Sep 2024 19:08:08 -0700 Subject: [PATCH 013/113] latest changes --- internal/tss/errors/errors.go | 9 + internal/tss/services/transaction_service.go | 145 +++-- .../tss/services/transaction_service_test.go | 529 +++++++++++++++++- 3 files changed, 607 insertions(+), 76 deletions(-) create mode 100644 internal/tss/errors/errors.go diff --git a/internal/tss/errors/errors.go b/internal/tss/errors/errors.go new file mode 100644 index 0000000..8cb3bc9 --- /dev/null +++ b/internal/tss/errors/errors.go @@ -0,0 +1,9 @@ +package errors + +import ( + "errors" +) + +var ( + OriginalXdrMalformed = errors.New("transaction string is malformed") +) diff --git a/internal/tss/services/transaction_service.go b/internal/tss/services/transaction_service.go index beadd31..c7ab4d0 100644 --- a/internal/tss/services/transaction_service.go +++ b/internal/tss/services/transaction_service.go @@ -6,14 +6,23 @@ import ( "encoding/json" "fmt" "io" - "log" "net/http" + "strconv" "github.com/stellar/go/clients/horizonclient" "github.com/stellar/go/txnbuild" "github.com/stellar/go/xdr" "github.com/stellar/wallet-backend/internal/signing" "github.com/stellar/wallet-backend/internal/tss" + tssErr "github.com/stellar/wallet-backend/internal/tss/errors" +) + +var ( + RpcPost = http.Post + UnMarshalRPCResponse = io.ReadAll + UnMarshalJSON = parseJSONBody + callRPC = sendRPCRequest + UnMarshalErrorResultXdr = parseErrorResultXdr ) type transactionService struct { @@ -32,7 +41,51 @@ type TransactionServiceOptions struct { BaseFee int64 } -func (o *TransactionServiceOptions) Validate() error { +func parseJSONBody(body []byte) (map[string]interface{}, error) { + var res map[string]interface{} + err := json.Unmarshal(body, &res) + if err != nil { + return nil, fmt.Errorf(err.Error()) + } + return res, nil +} + +func parseErrorResultXdr(errorResultXdr string) (string, error) { + errorResult := xdr.TransactionResult{} + err := errorResult.UnmarshalBinary([]byte(errorResultXdr)) + if err != nil { + return "", fmt.Errorf("SendTransaction: unable to unmarshal errorResultXdr: %s", errorResultXdr) + } + return errorResult.Result.Code.String(), nil +} + +func sendRPCRequest(rpcUrl string, method string, params map[string]string) (map[string]interface{}, error) { + payload := map[string]interface{}{ + "jsonrpc": "2.0", + "id": 1, + "method": method, + "params": params, + } + jsonData, _ := json.Marshal(payload) + + resp, err := RpcPost(rpcUrl, "application/json", bytes.NewBuffer(jsonData)) + if err != nil { + return nil, fmt.Errorf(method+": sending POST request to rpc: %v", err) + } + defer resp.Body.Close() + + body, err := UnMarshalRPCResponse(resp.Body) + if err != nil { + return nil, fmt.Errorf(method+": unmarshalling rpc response: %v", err) + } + res, err := UnMarshalJSON(body) + if err != nil { + return nil, fmt.Errorf(method+": parsing rpc response JSON: %v", err) + } + return res, nil +} + +func (o *TransactionServiceOptions) ValidateOptions() error { if o.DistributionAccountSignatureClient == nil { return fmt.Errorf("distribution account signature client cannot be nil") } @@ -58,11 +111,11 @@ func (o *TransactionServiceOptions) Validate() error { func (t *transactionService) SignAndBuildNewTransaction(ctx context.Context, origTxXdr string) (*txnbuild.FeeBumpTransaction, error) { genericTx, err := txnbuild.TransactionFromXDR(origTxXdr) if err != nil { - return nil, fmt.Errorf("deserializing the transaction xdr: %w", err) + return nil, tssErr.OriginalXdrMalformed } originalTx, txEmpty := genericTx.Transaction() - if txEmpty { - return nil, fmt.Errorf("empty transaction: %w", err) + if !txEmpty { + return nil, tssErr.OriginalXdrMalformed } channelAccountPublicKey, err := t.ChannelAccountSignatureClient.GetAccountPublicKey(ctx) if err != nil { @@ -70,7 +123,7 @@ func (t *transactionService) SignAndBuildNewTransaction(ctx context.Context, ori } channelAccount, err := t.HorizonClient.AccountDetail(horizonclient.AccountRequest{AccountID: channelAccountPublicKey}) if err != nil { - return nil, fmt.Errorf("getting channel account details: %w", err) + return nil, fmt.Errorf("getting channel account details from horizon: %w", err) } tx, err := txnbuild.NewTransaction( txnbuild.TransactionParams{ @@ -95,6 +148,7 @@ func (t *transactionService) SignAndBuildNewTransaction(ctx context.Context, ori if err != nil { return nil, fmt.Errorf("getting distribution account public key: %w", err) } + feeBumpTx, err := txnbuild.NewFeeBumpTransaction( txnbuild.FeeBumpTransactionParams{ Inner: tx, @@ -105,6 +159,7 @@ func (t *transactionService) SignAndBuildNewTransaction(ctx context.Context, ori if err != nil { return nil, fmt.Errorf("building fee-bump transaction %w", err) } + feeBumpTx, err = t.DistributionAccountSignatureClient.SignStellarFeeBumpTransaction(ctx, feeBumpTx) if err != nil { return nil, fmt.Errorf("signing the fee bump transaction with distribution account: %w", err) @@ -113,59 +168,57 @@ func (t *transactionService) SignAndBuildNewTransaction(ctx context.Context, ori } func (t *transactionService) SendTransaction(transactionXdr string) (tss.RPCSendTxResponse, error) { - payload := map[string]interface{}{ - "jsonrpc": "2.0", - "id": 1, - "method": "sendTransaction", - "params": map[string]string{ - "transaction": transactionXdr, - }, - } - jsonData, _ := json.Marshal(payload) - - resp, err := http.Post(t.RpcUrl, "application/json", bytes.NewBuffer(jsonData)) + rpcResponse, err := callRPC(t.RpcUrl, "sendTransaction", map[string]string{"transaction": transactionXdr}) if err != nil { - return tss.RPCSendTxResponse{}, fmt.Errorf("sending POST request to rpc: %v", err) - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return tss.RPCSendTxResponse{}, fmt.Errorf("reading rpc response: %v", err) - } - var res map[string]interface{} - err = json.Unmarshal(body, &res) - if err != nil { - return tss.RPCSendTxResponse{}, fmt.Errorf("parsing rpc response JSON: %v", err) + return tss.RPCSendTxResponse{}, fmt.Errorf(err.Error()) } sendTxResponse := tss.RPCSendTxResponse{} - if result, ok := res["result"].(map[string]interface{}); ok { - if val, exists := result["errorResultXdr"].(string); exists { - errorResult := xdr.TransactionResult{} - errorResult.UnmarshalBinary([]byte(val)) - sendTxResponse.ErrorCode = errorResult.Result.Code.String() - } + if result, ok := rpcResponse["result"].(map[string]interface{}); ok { if val, exists := result["status"].(string); exists { sendTxResponse.Status = val } + if val, exists := result["errorResultXdr"].(string); exists { + errorCode, err := UnMarshalErrorResultXdr(val) + if err != nil { + return sendTxResponse, fmt.Errorf("SendTransaction: unable to unmarshal errorResultXdr: %s", val) + } + sendTxResponse.ErrorCode = errorCode + } } - fmt.Println(sendTxResponse) - prettyResponse, err := json.MarshalIndent(res, "", " ") - if err != nil { - log.Fatalf("Error formatting rpc JSON response: %v", err) - } - fmt.Println(string(prettyResponse)) return sendTxResponse, nil } -func NewTransactionService(opts TransactionServiceOptions) (*transactionService, error) { - /* - if err := opts.Validate(); err != nil { - return nil, err +func (t *transactionService) GetTransaction(transactionHash string) (tss.RPCGetIngestTxResponse, error) { + rpcResponse, err := callRPC(t.RpcUrl, "getTransaction", map[string]string{"hash": transactionHash}) + if err != nil { + return tss.RPCGetIngestTxResponse{}, fmt.Errorf(err.Error()) + } + + getIngestTxResponse := tss.RPCGetIngestTxResponse{} + if result, ok := rpcResponse["result"].(map[string]interface{}); ok { + if status, exists := result["status"].(string); exists { + getIngestTxResponse.Status = status } - */ + if envelopeXdr, exists := result["envelopeXdr"].(string); exists { + getIngestTxResponse.EnvelopeXdr = envelopeXdr + } + if resultXdr, exists := result["resultXdr"].(string); exists { + getIngestTxResponse.ResultXdr = resultXdr + } + if createdAt, exists := result["createdAt"].(string); exists { + // we can supress erroneous createdAt errors as this is not an important field + createdAtInt, _ := strconv.ParseInt(createdAt, 10, 64) + getIngestTxResponse.CreatedAt = createdAtInt + } + } + return getIngestTxResponse, nil +} +func NewTransactionService(opts TransactionServiceOptions) (*transactionService, error) { + if err := opts.ValidateOptions(); err != nil { + return nil, err + } return &transactionService{ DistributionAccountSignatureClient: opts.DistributionAccountSignatureClient, ChannelAccountSignatureClient: opts.ChannelAccountSignatureClient, diff --git a/internal/tss/services/transaction_service_test.go b/internal/tss/services/transaction_service_test.go index 53b3ead..eebee35 100644 --- a/internal/tss/services/transaction_service_test.go +++ b/internal/tss/services/transaction_service_test.go @@ -1,42 +1,24 @@ package tss_services import ( - "fmt" + "bytes" + "context" + "encoding/json" + "errors" + "io" + "net/http" "testing" + "github.com/stellar/go/clients/horizonclient" "github.com/stellar/go/keypair" + "github.com/stellar/go/protocols/horizon" "github.com/stellar/go/txnbuild" + "github.com/stellar/wallet-backend/internal/signing" + tssErr "github.com/stellar/wallet-backend/internal/tss/errors" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" ) -func TestThis(t *testing.T) { - - tx := buildTestTransaction() - - tsStr, _ := tx.Base64() - oTx, err := txnbuild.TransactionFromXDR(tsStr) - e := false - if err != nil { - e = true - } - assert.False(t, e) - tt, _ := oTx.Transaction() - fmt.Println("Base fee boo: ", tt.BaseFee()) -} - -func TestSendTransaction(t *testing.T) { - txService, _ := NewTransactionService(TransactionServiceOptions{ - DistributionAccountSignatureClient: nil, - ChannelAccountSignatureClient: nil, - HorizonClient: nil, - RpcUrl: "http://localhost:8000/soroban/rpc", - BaseFee: 114, - }) - txStr, _ := buildTestTransaction().Base64() - txService.SendTransaction(txStr) - -} - func buildTestTransaction() *txnbuild.Transaction { accountToSponsor := keypair.MustRandom() @@ -53,10 +35,497 @@ func buildTestTransaction() *txnbuild.Transaction { Asset: txnbuild.NativeAsset{}, }, }, - //BaseFee: txnbuild.MinBaseFee, BaseFee: 104, Preconditions: txnbuild.Preconditions{TimeBounds: txnbuild.NewTimeout(10)}, }) return tx +} + +func TestValidate(t *testing.T) { + +} + +func TestSignAndBuildNewTransaction(t *testing.T) { + distributionAccountSignatureClient := signing.SignatureClientMock{} + defer distributionAccountSignatureClient.AssertExpectations(t) + channelAccountSignatureClient := signing.SignatureClientMock{} + defer channelAccountSignatureClient.AssertExpectations(t) + horizonClient := horizonclient.MockClient{} + defer horizonClient.AssertExpectations(t) + txService, _ := NewTransactionService(TransactionServiceOptions{ + DistributionAccountSignatureClient: &distributionAccountSignatureClient, + ChannelAccountSignatureClient: &channelAccountSignatureClient, + HorizonClient: &horizonClient, + RpcUrl: "http://localhost:8000/soroban/rpc", + BaseFee: 114, + }) + + txStr, _ := buildTestTransaction().Base64() + + t.Run("malformed_transaction_string", func(t *testing.T) { + feeBumpTx, err := txService.SignAndBuildNewTransaction(context.Background(), "abcd") + assert.Empty(t, feeBumpTx) + assert.ErrorIs(t, tssErr.OriginalXdrMalformed, err) + }) + + t.Run("channel_account_signature_client_get_account_public_key_err", func(t *testing.T) { + channelAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return("", errors.New("channel accounts unavailable")). + Once() + + feeBumpTx, err := txService.SignAndBuildNewTransaction(context.Background(), txStr) + assert.Empty(t, feeBumpTx) + assert.Equal(t, "getting channel account public key: channel accounts unavailable", err.Error()) + }) + + t.Run("horizon_client_get_account_detail_err", func(t *testing.T) { + channelAccount := keypair.MustRandom() + channelAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return(channelAccount.Address(), nil). + Once() + + horizonClient. + On("AccountDetail", horizonclient.AccountRequest{ + AccountID: channelAccount.Address(), + }). + Return(horizon.Account{}, errors.New("horizon down")). + Once() + + feeBumpTx, err := txService.SignAndBuildNewTransaction(context.Background(), txStr) + assert.Empty(t, feeBumpTx) + assert.Equal(t, "getting channel account details from horizon: horizon down", err.Error()) + }) + + t.Run("horizon_client_sign_stellar_transaction_w_channel_account_err", func(t *testing.T) { + channelAccount := keypair.MustRandom() + channelAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return(channelAccount.Address(), nil). + Once(). + On("SignStellarTransaction", context.Background(), mock.AnythingOfType("*txnbuild.Transaction"), []string{channelAccount.Address()}). + Return(nil, errors.New("unable to sign")). + Once() + horizonClient. + On("AccountDetail", horizonclient.AccountRequest{ + AccountID: channelAccount.Address(), + }). + Return(horizon.Account{AccountID: channelAccount.Address(), Sequence: 1}, nil). + Once() + + feeBumpTx, err := txService.SignAndBuildNewTransaction(context.Background(), txStr) + assert.Empty(t, feeBumpTx) + assert.Equal(t, "signing transaction with channel account: unable to sign", err.Error()) + }) + + t.Run("distribution_account_signature_client_get_account_public_key_err", func(t *testing.T) { + channelAccount := keypair.MustRandom() + signedTx := txnbuild.Transaction{} + channelAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return(channelAccount.Address(), nil). + Once(). + On("SignStellarTransaction", context.Background(), mock.AnythingOfType("*txnbuild.Transaction"), []string{channelAccount.Address()}). + Return(&signedTx, nil). + Once() + + distributionAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return("", errors.New("client down")). + Once() + + horizonClient. + On("AccountDetail", horizonclient.AccountRequest{ + AccountID: channelAccount.Address(), + }). + Return(horizon.Account{AccountID: channelAccount.Address(), Sequence: 1}, nil). + Once() + + feeBumpTx, err := txService.SignAndBuildNewTransaction(context.Background(), txStr) + assert.Empty(t, feeBumpTx) + assert.Equal(t, "getting distribution account public key: client down", err.Error()) + }) + + t.Run("horizon_client_sign_stellar_transaction_w_distribition_account_err", func(t *testing.T) { + account := keypair.MustRandom() + signedTx := buildTestTransaction() + channelAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return(account.Address(), nil). + Once(). + On("SignStellarTransaction", context.Background(), mock.AnythingOfType("*txnbuild.Transaction"), []string{account.Address()}). + Return(signedTx, nil). + Once() + + distributionAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return(account.Address(), nil). + Once(). + On("SignStellarFeeBumpTransaction", context.Background(), mock.AnythingOfType("*txnbuild.FeeBumpTransaction")). + Return(nil, errors.New("unable to sign")). + Once() + + horizonClient. + On("AccountDetail", horizonclient.AccountRequest{ + AccountID: account.Address(), + }). + Return(horizon.Account{AccountID: account.Address(), Sequence: 1}, nil). + Once() + + feeBumpTx, err := txService.SignAndBuildNewTransaction(context.Background(), txStr) + assert.Empty(t, feeBumpTx) + assert.Equal(t, "signing the fee bump transaction with distribution account: unable to sign", err.Error()) + }) + + t.Run("returns_signed_tx", func(t *testing.T) { + account := keypair.MustRandom() + signedTx := buildTestTransaction() + testFeeBumpTx, _ := txnbuild.NewFeeBumpTransaction( + txnbuild.FeeBumpTransactionParams{ + Inner: signedTx, + FeeAccount: account.Address(), + BaseFee: int64(100), + }, + ) + channelAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return(account.Address(), nil). + Once(). + On("SignStellarTransaction", context.Background(), mock.AnythingOfType("*txnbuild.Transaction"), []string{account.Address()}). + Return(signedTx, nil). + Once() + + distributionAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return(account.Address(), nil). + Once(). + On("SignStellarFeeBumpTransaction", context.Background(), mock.AnythingOfType("*txnbuild.FeeBumpTransaction")). + Return(testFeeBumpTx, nil). + Once() + + horizonClient. + On("AccountDetail", horizonclient.AccountRequest{ + AccountID: account.Address(), + }). + Return(horizon.Account{AccountID: account.Address(), Sequence: 1}, nil). + Once() + + feeBumpTx, err := txService.SignAndBuildNewTransaction(context.Background(), txStr) + assert.Equal(t, feeBumpTx, testFeeBumpTx) + assert.Empty(t, err) + }) +} + +type MockPost struct { + mock.Mock +} + +func (m *MockPost) Post(url string, content string, body io.Reader) (*http.Response, error) { + args := m.Called(url, content, body) + return args.Get(0).(*http.Response), args.Error(1) +} + +type MockUnMarshallRPCResponse struct { + mock.Mock +} + +func (m *MockUnMarshallRPCResponse) ReadAll(r io.Reader) ([]byte, error) { + args := m.Called(r) + return args.Get(0).(([]byte)), args.Error(1) + +} + +type MockUnMarshalJSON struct { + mock.Mock +} + +func (m *MockUnMarshalJSON) UnMarshalJSONBody(body []byte) (map[string]interface{}, error) { + args := m.Called(body) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(map[string]interface{}), args.Error(1) +} + +func TestCallRPC(t *testing.T) { + mockPost := MockPost{} + RpcPost = mockPost.Post + defer func() { RpcPost = http.Post }() + mockUnMarshalRPCResponse := MockUnMarshallRPCResponse{} + UnMarshalRPCResponse = mockUnMarshalRPCResponse.ReadAll + defer func() { UnMarshalRPCResponse = io.ReadAll }() + mockUnMarshalJSON := MockUnMarshalJSON{} + UnMarshalJSON = mockUnMarshalJSON.UnMarshalJSONBody + defer func() { UnMarshalJSON = parseJSONBody }() + params := map[string]string{"transaction": "ABCD"} + payload := map[string]interface{}{ + "jsonrpc": "2.0", + "id": 1, + "method": "sendTransaction", + "params": params, + } + jsonData, _ := json.Marshal(payload) + rpcUrl := "http://localhost:8000/soroban/rpc" + + t.Run("rpc_post_call_fails", func(t *testing.T) { + mockPost. + On("Post", rpcUrl, "application/json", bytes.NewBuffer(jsonData)). + Return(&http.Response{}, errors.New("connection error")). + Once() + + response, err := callRPC(rpcUrl, "sendTransaction", params) + + assert.Empty(t, response) + assert.Equal(t, "sendTransaction: sending POST request to rpc: connection error", err.Error()) + }) + t.Run("unmarshal_rpc_response_fails", func(t *testing.T) { + mockResponse := &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(`{"mock": "response"}`)), + } + + mockPost. + On("Post", rpcUrl, "application/json", bytes.NewBuffer(jsonData)). + Return(mockResponse, nil). + Once() + + mockUnMarshalRPCResponse. + On("ReadAll", mockResponse.Body). + Return([]byte{}, errors.New("bad string")). + Once() + + response, err := callRPC(rpcUrl, "sendTransaction", params) + + assert.Empty(t, response) + assert.Equal(t, "sendTransaction: unmarshalling rpc response: bad string", err.Error()) + }) + + t.Run("unmarshal_json_fails", func(t *testing.T) { + mockResponse := &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(`{"mock": "response"}`)), + } + + mockPost. + On("Post", rpcUrl, "application/json", mock.AnythingOfType("*bytes.Buffer")). + Return(mockResponse, nil). + Once() + + body := []byte("response") + mockUnMarshalRPCResponse. + On("ReadAll", mockResponse.Body). + Return(body, nil). + Once() + + mockUnMarshalJSON. + On("UnMarshalJSONBody", body). + Return(nil, errors.New("bad json format")). + Once() + + response, err := callRPC(rpcUrl, "sendTransaction", params) + + assert.Empty(t, response) + assert.Equal(t, "sendTransaction: parsing rpc response JSON: bad json format", err.Error()) + }) + t.Run("returns_unmarshalled_value", func(t *testing.T) { + mockResponse := &http.Response{ + StatusCode: 200, + Body: io.NopCloser(bytes.NewBufferString(`{"mock": "response"}`)), + } + + mockPost. + On("Post", rpcUrl, "application/json", mock.AnythingOfType("*bytes.Buffer")). + Return(mockResponse, nil). + Once() + + body := []byte("response") + mockUnMarshalRPCResponse. + On("ReadAll", mockResponse.Body). + Return(body, nil). + Once() + + expectedResponse := map[string]interface{}{"jsonrpc": "2.0", "id": 1, "result": map[string]interface{}{"status": "SUCCESS", "envelopeXdr": "ABCD"}} + + mockUnMarshalJSON. + On("UnMarshalJSONBody", body). + Return(expectedResponse, nil). + Once() + + rpcResponse, err := callRPC(rpcUrl, "sendTransaction", params) + + assert.Equal(t, rpcResponse, expectedResponse) + assert.Empty(t, err) + }) +} + +type MockCallRPC struct { + mock.Mock +} + +func (m *MockCallRPC) callRPC(rpcUrl string, method string, params map[string]string) (map[string]interface{}, error) { + args := m.Called(rpcUrl, method, params) + if args.Get(0) == nil { + return nil, args.Error(1) + } + return args.Get(0).(map[string]interface{}), args.Error(1) +} + +type MockUnMarshalErrorResultXdr struct { + mock.Mock +} + +func (m *MockUnMarshalErrorResultXdr) UnMarshalErrorResultXdr(errorResultXdr string) (string, error) { + args := m.Called(errorResultXdr) + return args.String(0), args.Error(1) +} + +func TestSendTransaction(t *testing.T) { + mockCallRPC := MockCallRPC{} + callRPC = mockCallRPC.callRPC + defer func() { callRPC = sendRPCRequest }() + mockUnMarshalErrorResultXdr := MockUnMarshalErrorResultXdr{} + UnMarshalErrorResultXdr = mockUnMarshalErrorResultXdr.UnMarshalErrorResultXdr + defer func() { UnMarshalErrorResultXdr = parseErrorResultXdr }() + txService, _ := NewTransactionService(TransactionServiceOptions{ + DistributionAccountSignatureClient: &signing.SignatureClientMock{}, + ChannelAccountSignatureClient: &signing.SignatureClientMock{}, + HorizonClient: &horizonclient.MockClient{}, + RpcUrl: "http://localhost:8000/soroban/rpc", + BaseFee: 114, + }) + txXdr, _ := buildTestTransaction().Base64() + rpcUrl := "http://localhost:8000/soroban/rpc" + + t.Run("call_rpc_returns_error", func(t *testing.T) { + mockCallRPC. + On("callRPC", rpcUrl, "sendTransaction", map[string]string{"transaction": txXdr}). + Return(nil, errors.New("unable to reach rpc server")). + Once() + + _, err := txService.SendTransaction(txXdr) + assert.Equal(t, "unable to reach rpc server", err.Error()) + }) + t.Run("error_unmarshaling_error_result_xdr", func(t *testing.T) { + errorResultXdr := "AAAAAAAAAGT////7AAAAAA==" + rpcResponse := map[string]interface{}{"jsonrpc": "2.0", "id": 1, "result": map[string]interface{}{"status": "ERROR", "errorResultXdr": errorResultXdr}} + mockCallRPC. + On("callRPC", rpcUrl, "sendTransaction", map[string]string{"transaction": txXdr}). + Return(rpcResponse, nil). + Once() + + mockUnMarshalErrorResultXdr. + On("UnMarshalErrorResultXdr", errorResultXdr). + Return("", errors.New("unable to unmarshal")). + Once() + + rpcSendTxResponse, err := txService.SendTransaction(txXdr) + assert.Equal(t, rpcSendTxResponse.Status, "ERROR") + assert.Empty(t, rpcSendTxResponse.ErrorCode) + assert.Equal(t, "SendTransaction: unable to unmarshal errorResultXdr: "+errorResultXdr, err.Error()) + }) + t.Run("return_send_tx_response", func(t *testing.T) { + errorResultXdr := "AAAAAAAAAGT////7AAAAAA==" + rpcResponse := map[string]interface{}{"jsonrpc": "2.0", "id": 1, "result": map[string]interface{}{"status": "ERROR", "errorResultXdr": errorResultXdr}} + mockCallRPC. + On("callRPC", rpcUrl, "sendTransaction", map[string]string{"transaction": txXdr}). + Return(rpcResponse, nil). + Once() + + mockUnMarshalErrorResultXdr. + On("UnMarshalErrorResultXdr", errorResultXdr). + Return("txError", nil). + Once() + + rpcSendTxResponse, err := txService.SendTransaction(txXdr) + assert.Equal(t, rpcSendTxResponse.Status, "ERROR") + assert.Equal(t, rpcSendTxResponse.ErrorCode, "txError") + assert.Empty(t, err) + }) +} + +func TestGetTransaction(t *testing.T) { + mockCallRPC := MockCallRPC{} + callRPC = mockCallRPC.callRPC + defer func() { callRPC = sendRPCRequest }() + txService, _ := NewTransactionService(TransactionServiceOptions{ + DistributionAccountSignatureClient: &signing.SignatureClientMock{}, + ChannelAccountSignatureClient: &signing.SignatureClientMock{}, + HorizonClient: &horizonclient.MockClient{}, + RpcUrl: "http://localhost:8000/soroban/rpc", + BaseFee: 114, + }) + txHash, _ := buildTestTransaction().HashHex("abcd") + rpcUrl := "http://localhost:8000/soroban/rpc" + + t.Run("call_rpc_returns_error", func(t *testing.T) { + mockCallRPC. + On("callRPC", rpcUrl, "getTransaction", map[string]string{"hash": txHash}). + Return(nil, errors.New("unable to reach rpc server")). + Once() + + _, err := txService.GetTransaction(txHash) + assert.Equal(t, "unable to reach rpc server", err.Error()) + }) + + t.Run("returns_resp_with_status", func(t *testing.T) { + rpcResponse := map[string]interface{}{"jsonrpc": "2.0", "id": 1, "result": map[string]interface{}{"status": "ERROR"}} + mockCallRPC. + On("callRPC", rpcUrl, "getTransaction", map[string]string{"hash": txHash}). + Return(rpcResponse, nil). + Once() + + getIngestTxResponse, err := txService.GetTransaction(txHash) + assert.Equal(t, getIngestTxResponse.Status, "ERROR") + assert.Empty(t, getIngestTxResponse.EnvelopeXdr) + assert.Empty(t, getIngestTxResponse.ResultXdr) + assert.Empty(t, getIngestTxResponse.CreatedAt) + assert.Empty(t, err) + }) + + t.Run("returns_resp_with_envelope_xdr", func(t *testing.T) { + rpcResponse := map[string]interface{}{"jsonrpc": "2.0", "id": 1, "result": map[string]interface{}{"envelopeXdr": "abcd"}} + mockCallRPC. + On("callRPC", rpcUrl, "getTransaction", map[string]string{"hash": txHash}). + Return(rpcResponse, nil). + Once() + + getIngestTxResponse, err := txService.GetTransaction(txHash) + assert.Empty(t, getIngestTxResponse.Status) + assert.Equal(t, getIngestTxResponse.EnvelopeXdr, "abcd") + assert.Empty(t, getIngestTxResponse.ResultXdr) + assert.Empty(t, getIngestTxResponse.CreatedAt) + assert.Empty(t, err) + }) + + t.Run("returns_resp_with_result_xdr", func(t *testing.T) { + rpcResponse := map[string]interface{}{"jsonrpc": "2.0", "id": 1, "result": map[string]interface{}{"resultXdr": "abcd"}} + mockCallRPC. + On("callRPC", rpcUrl, "getTransaction", map[string]string{"hash": txHash}). + Return(rpcResponse, nil). + Once() + + getIngestTxResponse, err := txService.GetTransaction(txHash) + assert.Empty(t, getIngestTxResponse.Status) + assert.Empty(t, getIngestTxResponse.EnvelopeXdr) + assert.Equal(t, getIngestTxResponse.ResultXdr, "abcd") + assert.Empty(t, getIngestTxResponse.CreatedAt) + assert.Empty(t, err) + }) + + t.Run("returns_resp_with_created_at", func(t *testing.T) { + rpcResponse := map[string]interface{}{"jsonrpc": "2.0", "id": 1, "result": map[string]interface{}{"createdAt": "1234"}} + mockCallRPC. + On("callRPC", rpcUrl, "getTransaction", map[string]string{"hash": txHash}). + Return(rpcResponse, nil). + Once() + + getIngestTxResponse, err := txService.GetTransaction(txHash) + assert.Empty(t, getIngestTxResponse.Status) + assert.Empty(t, getIngestTxResponse.EnvelopeXdr) + assert.Empty(t, getIngestTxResponse.ResultXdr) + assert.Equal(t, getIngestTxResponse.CreatedAt, int64(1234)) + assert.Empty(t, err) + }) } From 02c7de348f2086f4997117d5c5e404777d752e7b Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Sat, 7 Sep 2024 17:57:04 -0700 Subject: [PATCH 014/113] add tests for ValidateOptions --- internal/tss/services/transaction_service.go | 46 +++++++------- .../tss/services/transaction_service_test.go | 61 ++++++++++++++++++- 2 files changed, 83 insertions(+), 24 deletions(-) diff --git a/internal/tss/services/transaction_service.go b/internal/tss/services/transaction_service.go index c7ab4d0..8efde1d 100644 --- a/internal/tss/services/transaction_service.go +++ b/internal/tss/services/transaction_service.go @@ -41,6 +41,29 @@ type TransactionServiceOptions struct { BaseFee int64 } +func (o *TransactionServiceOptions) ValidateOptions() error { + if o.DistributionAccountSignatureClient == nil { + return fmt.Errorf("distribution account signature client cannot be nil") + } + + if o.ChannelAccountSignatureClient == nil { + return fmt.Errorf("channel account signature client cannot be nil") + } + + if o.HorizonClient == nil { + return fmt.Errorf("horizon client cannot be nil") + } + + if o.RpcUrl == "" { + return fmt.Errorf("rpc url cannot be empty") + } + + if o.BaseFee < int64(txnbuild.MinBaseFee) { + return fmt.Errorf("base fee is lower than the minimum network fee") + } + return nil +} + func parseJSONBody(body []byte) (map[string]interface{}, error) { var res map[string]interface{} err := json.Unmarshal(body, &res) @@ -85,29 +108,6 @@ func sendRPCRequest(rpcUrl string, method string, params map[string]string) (map return res, nil } -func (o *TransactionServiceOptions) ValidateOptions() error { - if o.DistributionAccountSignatureClient == nil { - return fmt.Errorf("distribution account signature client cannot be nil") - } - - if o.ChannelAccountSignatureClient == nil { - return fmt.Errorf("channel account signature client cannot be nil") - } - - if o.HorizonClient == nil { - return fmt.Errorf("horizon client cannot be nil") - } - - if o.RpcUrl == "" { - return fmt.Errorf("rpc url cannot be empty") - } - - if o.BaseFee < int64(txnbuild.MinBaseFee) { - return fmt.Errorf("base fee is lower than the minimum network fee") - } - return nil -} - func (t *transactionService) SignAndBuildNewTransaction(ctx context.Context, origTxXdr string) (*txnbuild.FeeBumpTransaction, error) { genericTx, err := txnbuild.TransactionFromXDR(origTxXdr) if err != nil { diff --git a/internal/tss/services/transaction_service_test.go b/internal/tss/services/transaction_service_test.go index eebee35..38f0d13 100644 --- a/internal/tss/services/transaction_service_test.go +++ b/internal/tss/services/transaction_service_test.go @@ -41,8 +41,67 @@ func buildTestTransaction() *txnbuild.Transaction { return tx } -func TestValidate(t *testing.T) { +func TestValidateOptions(t *testing.T) { + t.Run("return_error_when_distribution_signature_client_null", func(t *testing.T) { + opts := TransactionServiceOptions{ + DistributionAccountSignatureClient: nil, + ChannelAccountSignatureClient: &signing.SignatureClientMock{}, + HorizonClient: &horizonclient.MockClient{}, + RpcUrl: "http://localhost:8000/soroban/rpc", + BaseFee: 114, + } + err := opts.ValidateOptions() + assert.Equal(t, "distribution account signature client cannot be nil", err.Error()) + + }) + + t.Run("return_error_when_channel_signature_client_null", func(t *testing.T) { + opts := TransactionServiceOptions{ + DistributionAccountSignatureClient: &signing.SignatureClientMock{}, + ChannelAccountSignatureClient: nil, + HorizonClient: &horizonclient.MockClient{}, + RpcUrl: "http://localhost:8000/soroban/rpc", + BaseFee: 114, + } + err := opts.ValidateOptions() + assert.Equal(t, "channel account signature client cannot be nil", err.Error()) + }) + + t.Run("return_error_when_horizon_client_null", func(t *testing.T) { + opts := TransactionServiceOptions{ + DistributionAccountSignatureClient: &signing.SignatureClientMock{}, + ChannelAccountSignatureClient: &signing.SignatureClientMock{}, + HorizonClient: nil, + RpcUrl: "http://localhost:8000/soroban/rpc", + BaseFee: 114, + } + err := opts.ValidateOptions() + assert.Equal(t, "horizon client cannot be nil", err.Error()) + }) + t.Run("return_error_when_rpc_url_empty", func(t *testing.T) { + opts := TransactionServiceOptions{ + DistributionAccountSignatureClient: &signing.SignatureClientMock{}, + ChannelAccountSignatureClient: &signing.SignatureClientMock{}, + HorizonClient: &horizonclient.MockClient{}, + RpcUrl: "", + BaseFee: 114, + } + err := opts.ValidateOptions() + assert.Equal(t, "rpc url cannot be empty", err.Error()) + }) + + t.Run("return_error_when_base_fee_too_low", func(t *testing.T) { + opts := TransactionServiceOptions{ + DistributionAccountSignatureClient: &signing.SignatureClientMock{}, + ChannelAccountSignatureClient: &signing.SignatureClientMock{}, + HorizonClient: &horizonclient.MockClient{}, + RpcUrl: "http://localhost:8000/soroban/rpc", + BaseFee: txnbuild.MinBaseFee - 10, + } + err := opts.ValidateOptions() + assert.Equal(t, "base fee is lower than the minimum network fee", err.Error()) + }) } func TestSignAndBuildNewTransaction(t *testing.T) { From f9a56110184cd05d7219a8ff47e57d0d15a01b11 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Mon, 9 Sep 2024 18:27:11 -0700 Subject: [PATCH 015/113] changes based on comments --- .../2024-08-28.0-tss_transactions.sql | 22 ++++++------ internal/tss/channel.go | 36 +++++++++++++------ 2 files changed, 36 insertions(+), 22 deletions(-) diff --git a/internal/db/migrations/2024-08-28.0-tss_transactions.sql b/internal/db/migrations/2024-08-28.0-tss_transactions.sql index 02f2d36..0f03c3c 100644 --- a/internal/db/migrations/2024-08-28.0-tss_transactions.sql +++ b/internal/db/migrations/2024-08-28.0-tss_transactions.sql @@ -1,21 +1,21 @@ -- +migrate Up CREATE TABLE tss_transactions ( - transaction_hash VARCHAR(70) PRIMARY KEY, - transaction_xdr VARCHAR(400), - webhook_url VARCHAR(250), - current_status VARCHAR(50), - creation_time TIMESTAMPTZ DEFAULT NOW(), - last_updated_time TIMESTAMPTZ DEFAULT NOW(), + transaction_hash TEXT PRIMARY KEY, + transaction_xdr TEXT, + webhook_url TEXT, + current_status TEXT, + created_at TIMESTAMPTZ DEFAULT NOW(), + updated_at TIMESTAMPTZ DEFAULT NOW(), claimed_until TIMESTAMPTZ ); CREATE TABLE tss_transaction_submission_tries ( - original_transaction_hash VARCHAR(70), - try_transaction_hash VARCHAR(70), - try_transaction_xdr VARCHAR(400), - status VARCHAR(50), - last_updated TIMESTAMPTZ DEFAULT NOW() + original_transaction_hash TEXT, + try_transaction_hash TEXT, + try_transaction_xdr TEXT, + status TEXT, + updated_at TIMESTAMPTZ DEFAULT NOW() ); CREATE INDEX idx_tx_current_status ON tss_transactions(current_status); diff --git a/internal/tss/channel.go b/internal/tss/channel.go index 2a9096c..fe0060f 100644 --- a/internal/tss/channel.go +++ b/internal/tss/channel.go @@ -1,30 +1,44 @@ package tss +type RPCTXStatus string + +const ( + // RPC sendTransaction statuses + PendingStatus RPCTXStatus = "PENDING" + DuplicateStatus RPCTXStatus = "DUPLICATE" + TryAgainLaterStatus RPCTXStatus = "TRY_AGAIN_LATER" + ErrorStatus RPCTXStatus = "ERROR" + // RPC getTransaction(s) statuses + NotFoundStatus RPCTXStatus = "NOT_FOUND" + FailedStatus RPCTXStatus = "FAILED" + SuccessStatus RPCTXStatus = "SUCCESS" +) + type RPCGetIngestTxResponse struct { - // a status that indicated whether this transaction failed or successly made it to the ledger + // A status that indicated whether this transaction failed or successly made it to the ledger Status string - // the raw TransactionEnvelope XDR for this transaction - EnvelopeXdr string - // the raw TransactionResult XDR of the envelopeXdr - ResultXdr string + // The raw TransactionEnvelope XDR for this transaction + EnvelopeXDR string + // The raw TransactionResult XDR of the envelopeXdr + ResultXDR string // The unix timestamp of when the transaction was included in the ledger CreatedAt int64 } type RPCSendTxResponse struct { - // the status of an RPC sendTransaction call. Can be one of [PENDING, DUPLICATE, TRY_AGAIN_LATER, ERROR] - Status string - // the (optional) error code that is derived by deserialzing the errorResultXdr string in the sendTransaction response + // The status of an RPC sendTransaction call. Can be one of [PENDING, DUPLICATE, TRY_AGAIN_LATER, ERROR] + Status RPCTXStatus + // The (optional) error code that is derived by deserialzing the errorResultXdr string in the sendTransaction response // list of possible errror codes: https://developers.stellar.org/docs/data/horizon/api-reference/errors/result-codes/transactions ErrorCode string } type Payload struct { - // the hash of the transaction xdr submitted by the client - the id of the transaction submitted by a client + // The hash of the transaction xdr submitted by the client - the id of the transaction submitted by a client TransactionHash string - // relevant fields in an RPC sendTransaction response + // Relevant fields in an RPC sendTransaction response RpcSubmitTxResponse RPCSendTxResponse - // relevant fields in the transaction list inside the RPC getTransactions response + // Relevant fields in the transaction list inside the RPC getTransactions response RpcGetIngestTxResponse RPCGetIngestTxResponse } From c3e8de92d3f245d846f127eabd3ecddc3f6ff17e Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Mon, 9 Sep 2024 18:51:47 -0700 Subject: [PATCH 016/113] latest changes based on changes to interface --- internal/tss/channel.go | 2 +- internal/tss/services/transaction_service.go | 8 ++--- .../tss/services/transaction_service_test.go | 29 ++++++++++--------- 3 files changed, 20 insertions(+), 19 deletions(-) diff --git a/internal/tss/channel.go b/internal/tss/channel.go index fe0060f..6e3303e 100644 --- a/internal/tss/channel.go +++ b/internal/tss/channel.go @@ -16,7 +16,7 @@ const ( type RPCGetIngestTxResponse struct { // A status that indicated whether this transaction failed or successly made it to the ledger - Status string + Status RPCTXStatus // The raw TransactionEnvelope XDR for this transaction EnvelopeXDR string // The raw TransactionResult XDR of the envelopeXdr diff --git a/internal/tss/services/transaction_service.go b/internal/tss/services/transaction_service.go index 8efde1d..9a6d525 100644 --- a/internal/tss/services/transaction_service.go +++ b/internal/tss/services/transaction_service.go @@ -175,7 +175,7 @@ func (t *transactionService) SendTransaction(transactionXdr string) (tss.RPCSend sendTxResponse := tss.RPCSendTxResponse{} if result, ok := rpcResponse["result"].(map[string]interface{}); ok { - if val, exists := result["status"].(string); exists { + if val, exists := result["status"].(tss.RPCTXStatus); exists { sendTxResponse.Status = val } if val, exists := result["errorResultXdr"].(string); exists { @@ -197,14 +197,14 @@ func (t *transactionService) GetTransaction(transactionHash string) (tss.RPCGetI getIngestTxResponse := tss.RPCGetIngestTxResponse{} if result, ok := rpcResponse["result"].(map[string]interface{}); ok { - if status, exists := result["status"].(string); exists { + if status, exists := result["status"].(tss.RPCTXStatus); exists { getIngestTxResponse.Status = status } if envelopeXdr, exists := result["envelopeXdr"].(string); exists { - getIngestTxResponse.EnvelopeXdr = envelopeXdr + getIngestTxResponse.EnvelopeXDR = envelopeXdr } if resultXdr, exists := result["resultXdr"].(string); exists { - getIngestTxResponse.ResultXdr = resultXdr + getIngestTxResponse.ResultXDR = resultXdr } if createdAt, exists := result["createdAt"].(string); exists { // we can supress erroneous createdAt errors as this is not an important field diff --git a/internal/tss/services/transaction_service_test.go b/internal/tss/services/transaction_service_test.go index 38f0d13..0d2481f 100644 --- a/internal/tss/services/transaction_service_test.go +++ b/internal/tss/services/transaction_service_test.go @@ -14,6 +14,7 @@ import ( "github.com/stellar/go/protocols/horizon" "github.com/stellar/go/txnbuild" "github.com/stellar/wallet-backend/internal/signing" + "github.com/stellar/wallet-backend/internal/tss" tssErr "github.com/stellar/wallet-backend/internal/tss/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -468,7 +469,7 @@ func TestSendTransaction(t *testing.T) { }) t.Run("error_unmarshaling_error_result_xdr", func(t *testing.T) { errorResultXdr := "AAAAAAAAAGT////7AAAAAA==" - rpcResponse := map[string]interface{}{"jsonrpc": "2.0", "id": 1, "result": map[string]interface{}{"status": "ERROR", "errorResultXdr": errorResultXdr}} + rpcResponse := map[string]interface{}{"jsonrpc": "2.0", "id": 1, "result": map[string]interface{}{"status": tss.ErrorStatus, "errorResultXdr": errorResultXdr}} mockCallRPC. On("callRPC", rpcUrl, "sendTransaction", map[string]string{"transaction": txXdr}). Return(rpcResponse, nil). @@ -480,13 +481,13 @@ func TestSendTransaction(t *testing.T) { Once() rpcSendTxResponse, err := txService.SendTransaction(txXdr) - assert.Equal(t, rpcSendTxResponse.Status, "ERROR") + assert.Equal(t, rpcSendTxResponse.Status, tss.ErrorStatus) assert.Empty(t, rpcSendTxResponse.ErrorCode) assert.Equal(t, "SendTransaction: unable to unmarshal errorResultXdr: "+errorResultXdr, err.Error()) }) t.Run("return_send_tx_response", func(t *testing.T) { errorResultXdr := "AAAAAAAAAGT////7AAAAAA==" - rpcResponse := map[string]interface{}{"jsonrpc": "2.0", "id": 1, "result": map[string]interface{}{"status": "ERROR", "errorResultXdr": errorResultXdr}} + rpcResponse := map[string]interface{}{"jsonrpc": "2.0", "id": 1, "result": map[string]interface{}{"status": tss.ErrorStatus, "errorResultXdr": errorResultXdr}} mockCallRPC. On("callRPC", rpcUrl, "sendTransaction", map[string]string{"transaction": txXdr}). Return(rpcResponse, nil). @@ -498,7 +499,7 @@ func TestSendTransaction(t *testing.T) { Once() rpcSendTxResponse, err := txService.SendTransaction(txXdr) - assert.Equal(t, rpcSendTxResponse.Status, "ERROR") + assert.Equal(t, rpcSendTxResponse.Status, tss.ErrorStatus) assert.Equal(t, rpcSendTxResponse.ErrorCode, "txError") assert.Empty(t, err) }) @@ -529,16 +530,16 @@ func TestGetTransaction(t *testing.T) { }) t.Run("returns_resp_with_status", func(t *testing.T) { - rpcResponse := map[string]interface{}{"jsonrpc": "2.0", "id": 1, "result": map[string]interface{}{"status": "ERROR"}} + rpcResponse := map[string]interface{}{"jsonrpc": "2.0", "id": 1, "result": map[string]interface{}{"status": tss.SuccessStatus}} mockCallRPC. On("callRPC", rpcUrl, "getTransaction", map[string]string{"hash": txHash}). Return(rpcResponse, nil). Once() getIngestTxResponse, err := txService.GetTransaction(txHash) - assert.Equal(t, getIngestTxResponse.Status, "ERROR") - assert.Empty(t, getIngestTxResponse.EnvelopeXdr) - assert.Empty(t, getIngestTxResponse.ResultXdr) + assert.Equal(t, getIngestTxResponse.Status, tss.SuccessStatus) + assert.Empty(t, getIngestTxResponse.EnvelopeXDR) + assert.Empty(t, getIngestTxResponse.ResultXDR) assert.Empty(t, getIngestTxResponse.CreatedAt) assert.Empty(t, err) }) @@ -552,8 +553,8 @@ func TestGetTransaction(t *testing.T) { getIngestTxResponse, err := txService.GetTransaction(txHash) assert.Empty(t, getIngestTxResponse.Status) - assert.Equal(t, getIngestTxResponse.EnvelopeXdr, "abcd") - assert.Empty(t, getIngestTxResponse.ResultXdr) + assert.Equal(t, getIngestTxResponse.EnvelopeXDR, "abcd") + assert.Empty(t, getIngestTxResponse.ResultXDR) assert.Empty(t, getIngestTxResponse.CreatedAt) assert.Empty(t, err) }) @@ -567,8 +568,8 @@ func TestGetTransaction(t *testing.T) { getIngestTxResponse, err := txService.GetTransaction(txHash) assert.Empty(t, getIngestTxResponse.Status) - assert.Empty(t, getIngestTxResponse.EnvelopeXdr) - assert.Equal(t, getIngestTxResponse.ResultXdr, "abcd") + assert.Empty(t, getIngestTxResponse.EnvelopeXDR) + assert.Equal(t, getIngestTxResponse.ResultXDR, "abcd") assert.Empty(t, getIngestTxResponse.CreatedAt) assert.Empty(t, err) }) @@ -582,8 +583,8 @@ func TestGetTransaction(t *testing.T) { getIngestTxResponse, err := txService.GetTransaction(txHash) assert.Empty(t, getIngestTxResponse.Status) - assert.Empty(t, getIngestTxResponse.EnvelopeXdr) - assert.Empty(t, getIngestTxResponse.ResultXdr) + assert.Empty(t, getIngestTxResponse.EnvelopeXDR) + assert.Empty(t, getIngestTxResponse.ResultXDR) assert.Equal(t, getIngestTxResponse.CreatedAt, int64(1234)) assert.Empty(t, err) }) From 82b12fb9988eb51b37ccfaa878d570a6eb7e5c71 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Mon, 9 Sep 2024 18:52:17 -0700 Subject: [PATCH 017/113] string -> RPCTXStatus --- internal/tss/channel.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/tss/channel.go b/internal/tss/channel.go index fe0060f..6e3303e 100644 --- a/internal/tss/channel.go +++ b/internal/tss/channel.go @@ -16,7 +16,7 @@ const ( type RPCGetIngestTxResponse struct { // A status that indicated whether this transaction failed or successly made it to the ledger - Status string + Status RPCTXStatus // The raw TransactionEnvelope XDR for this transaction EnvelopeXDR string // The raw TransactionResult XDR of the envelopeXdr From 0662da9005ed6fdeb7c8522a4758fb62cac7e37f Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Tue, 10 Sep 2024 22:36:34 -0700 Subject: [PATCH 018/113] adding a transaction_builder.go which takes a list of operation xdrs and builds a transaction out of it --- internal/tss/utils/transaction_builder.go | 48 +++++++++++++++ .../tss/utils/transaction_builder_test.go | 61 +++++++++++++++++++ 2 files changed, 109 insertions(+) create mode 100644 internal/tss/utils/transaction_builder.go create mode 100644 internal/tss/utils/transaction_builder_test.go diff --git a/internal/tss/utils/transaction_builder.go b/internal/tss/utils/transaction_builder.go new file mode 100644 index 0000000..ea1702c --- /dev/null +++ b/internal/tss/utils/transaction_builder.go @@ -0,0 +1,48 @@ +package utils + +import ( + "encoding/base64" + "fmt" + "strings" + + xdr3 "github.com/stellar/go-xdr/xdr3" + "github.com/stellar/go/keypair" + "github.com/stellar/go/txnbuild" + "github.com/stellar/go/xdr" +) + +func BuildOriginalTransaction(txOpXDRs []string) (*txnbuild.Transaction, error) { + var operations []txnbuild.Operation + for _, opXDR := range txOpXDRs { + decodedBytes, err := base64.StdEncoding.DecodeString(opXDR) + if err != nil { + return nil, fmt.Errorf("decoding Operation XDR string") + } + dec := xdr3.NewDecoder(strings.NewReader(string(decodedBytes))) + var decodedOp xdr.Operation + dec.Decode(&decodedOp) + // for now, we assume that all operations are Payment operations + paymentOp := txnbuild.Payment{} + err = paymentOp.FromXDR(decodedOp) + if err != nil { + return nil, fmt.Errorf("unmarshaling xdr into Operation: %w", err) + } + err = paymentOp.Validate() + if err != nil { + return nil, fmt.Errorf("invalid Operation: %w", err) + } + operations = append(operations, &paymentOp) + } + + tx, _ := txnbuild.NewTransaction(txnbuild.TransactionParams{ + SourceAccount: &txnbuild.SimpleAccount{ + AccountID: keypair.MustRandom().Address(), + Sequence: 123, + }, + IncrementSequenceNum: true, + Operations: operations, + BaseFee: 104, + Preconditions: txnbuild.Preconditions{TimeBounds: txnbuild.NewTimeout(10)}, + }) + return tx, nil +} diff --git a/internal/tss/utils/transaction_builder_test.go b/internal/tss/utils/transaction_builder_test.go new file mode 100644 index 0000000..a0df8fa --- /dev/null +++ b/internal/tss/utils/transaction_builder_test.go @@ -0,0 +1,61 @@ +package utils + +import ( + "encoding/base64" + "strings" + "testing" + + xdr3 "github.com/stellar/go-xdr/xdr3" + "github.com/stellar/go/keypair" + "github.com/stellar/go/txnbuild" + "github.com/stretchr/testify/assert" +) + +func TestBuildOriginalTransaction(t *testing.T) { + t.Run("return_error_when_unable_to_decode_operation_xdr_string", func(t *testing.T) { + _, err := BuildOriginalTransaction([]string{"this@is#not$valid!"}) + assert.Equal(t, "decoding Operation XDR string", err.Error()) + + }) + t.Run("return_error_when_unable_to_unmarshal_xdr_into_operation", func(t *testing.T) { + ca := txnbuild.CreateAccount{ + Destination: keypair.MustRandom().Address(), + Amount: "10", + SourceAccount: keypair.MustRandom().Address(), + } + caOp, _ := ca.BuildXDR() + + buf := strings.Builder{} + enc := xdr3.NewEncoder(&buf) + caOp.EncodeTo(enc) + + caOpXDR := buf.String() + caOpXDRBase64 := base64.StdEncoding.EncodeToString([]byte(caOpXDR)) + + _, err := BuildOriginalTransaction([]string{caOpXDRBase64}) + assert.Equal(t, "unmarshaling xdr into Operation: error parsing payment operation from xdr", err.Error()) + }) + + t.Run("return_expected_transaction", func(t *testing.T) { + srcAccount := keypair.MustRandom().Address() + p := txnbuild.Payment{ + Destination: keypair.MustRandom().Address(), + Amount: "10", + Asset: txnbuild.NativeAsset{}, + SourceAccount: srcAccount, + } + op, _ := p.BuildXDR() + + var buf strings.Builder + enc := xdr3.NewEncoder(&buf) + op.EncodeTo(enc) + + opXDR := buf.String() + opXDRBase64 := base64.StdEncoding.EncodeToString([]byte(opXDR)) + tx, err := BuildOriginalTransaction([]string{opXDRBase64}) + firstOp := tx.Operations()[0] + assert.Equal(t, firstOp.GetSourceAccount(), srcAccount) + assert.Empty(t, err) + }) + +} From 8f76d61e6ea2c207a24ab0be065a44d71eb6dedf Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Wed, 11 Sep 2024 11:58:40 -0700 Subject: [PATCH 019/113] moving transaction_service to the utils dir --- internal/tss/{services => utils}/transaction_service.go | 2 +- internal/tss/{services => utils}/transaction_service_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename internal/tss/{services => utils}/transaction_service.go (99%) rename internal/tss/{services => utils}/transaction_service_test.go (99%) diff --git a/internal/tss/services/transaction_service.go b/internal/tss/utils/transaction_service.go similarity index 99% rename from internal/tss/services/transaction_service.go rename to internal/tss/utils/transaction_service.go index 9a6d525..66639d5 100644 --- a/internal/tss/services/transaction_service.go +++ b/internal/tss/utils/transaction_service.go @@ -1,4 +1,4 @@ -package tss_services +package utils import ( "bytes" diff --git a/internal/tss/services/transaction_service_test.go b/internal/tss/utils/transaction_service_test.go similarity index 99% rename from internal/tss/services/transaction_service_test.go rename to internal/tss/utils/transaction_service_test.go index 0d2481f..3a37cb4 100644 --- a/internal/tss/services/transaction_service_test.go +++ b/internal/tss/utils/transaction_service_test.go @@ -1,4 +1,4 @@ -package tss_services +package utils import ( "bytes" From 456b71f789d5bd118d83033247d4f875c24664c3 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Wed, 11 Sep 2024 16:38:45 -0700 Subject: [PATCH 020/113] upper case Channel methods --- internal/tss/channel.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/tss/channel.go b/internal/tss/channel.go index 6e3303e..205a485 100644 --- a/internal/tss/channel.go +++ b/internal/tss/channel.go @@ -43,6 +43,6 @@ type Payload struct { } type Channel interface { - send(payload Payload) - receive(payload Payload) + Send(payload Payload) + Receive(payload Payload) } From 5bdf8f22a98e163c306204a97df20a5747ca465f Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Thu, 12 Sep 2024 10:52:09 -0700 Subject: [PATCH 021/113] latest changes --- go.mod | 1 + go.sum | 2 + .../channels/rpc_caller_service_channel.go | 112 ++++++++++++++++++ internal/tss/services/rpc_caller_service.go | 19 +++ internal/tss/services/types.go | 7 ++ internal/tss/store/store.go | 49 ++++++++ internal/tss/store/types.go | 10 ++ internal/tss/{channel.go => types.go} | 16 ++- internal/tss/utils/transaction_service.go | 45 ++++--- .../tss/utils/transaction_service_test.go | 4 +- internal/tss/utils/types.go | 12 ++ 11 files changed, 255 insertions(+), 22 deletions(-) create mode 100644 internal/tss/channels/rpc_caller_service_channel.go create mode 100644 internal/tss/services/rpc_caller_service.go create mode 100644 internal/tss/services/types.go create mode 100644 internal/tss/store/store.go create mode 100644 internal/tss/store/types.go rename internal/tss/{channel.go => types.go} (83%) create mode 100644 internal/tss/utils/types.go diff --git a/go.mod b/go.mod index 66fd755..483dc6b 100644 --- a/go.mod +++ b/go.mod @@ -27,6 +27,7 @@ require ( github.com/BurntSushi/toml v1.3.2 // indirect github.com/Masterminds/squirrel v1.5.4 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect + github.com/alitto/pond v1.9.2 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.2.1 // indirect diff --git a/go.sum b/go.sum index d65c67e..809f167 100644 --- a/go.sum +++ b/go.sum @@ -56,6 +56,8 @@ github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migc github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/ajg/form v0.0.0-20160822230020-523a5da1a92f h1:zvClvFQwU++UpIUBGC8YmDlfhUrweEy1R1Fj1gu5iIM= github.com/ajg/form v0.0.0-20160822230020-523a5da1a92f/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= +github.com/alitto/pond v1.9.2 h1:9Qb75z/scEZVCoSU+osVmQ0I0JOeLfdTDafrbcJ8CLs= +github.com/alitto/pond v1.9.2/go.mod h1:xQn3P/sHTYcU/1BR3i86IGIrilcrGC2LiS+E2+CJWsI= github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= diff --git a/internal/tss/channels/rpc_caller_service_channel.go b/internal/tss/channels/rpc_caller_service_channel.go new file mode 100644 index 0000000..51815a3 --- /dev/null +++ b/internal/tss/channels/rpc_caller_service_channel.go @@ -0,0 +1,112 @@ +package channels + +import ( + "github.com/alitto/pond" + "github.com/stellar/wallet-backend/internal/tss" + tss_services "github.com/stellar/wallet-backend/internal/tss/services" + tss_store "github.com/stellar/wallet-backend/internal/tss/store" + "github.com/stellar/wallet-backend/internal/tss/utils" +) + +type rpcCallerServicePool struct { + pool *pond.WorkerPool + // some pool config, make a config struct for it + txService utils.TransactionService + errHandlerService tss_services.Service + store tss_store.Store +} + +func NewRPCCallerServiceChannel(store tss_store.Store, txService utils.TransactionService) (tss.Channel, error) { + pool := pond.New(10, 0, pond.MinWorkers(10)) + return &rpcCallerServicePool{ + pool: pool, + txService: txService, + store: store, + }, nil +} + +func (p *rpcCallerServicePool) Send(payload tss.Payload) { + p.pool.Submit(func() { + p.Receive(payload) + }) +} + +func (p *rpcCallerServicePool) Receive(payload tss.Payload) { + // maybe: why is sqlsqlExec db.SQLExecuter being passed GetAllByPublicKey in channel_accounts_model.go ? + err := p.store.UpsertTransaction(payload.ClientID, payload.TransactionHash, payload.TransactionXDR, tss.NewStatus) + if err != nil { + // TODO: log error + return + } + + feeBumpTx, err := p.txService.SignAndBuildNewTransaction(payload.TransactionXDR) + if err != nil { + // TODO: log error + return + } + feeBumpTxHash, err := feeBumpTx.HashHex("need network pass") + if err != nil { + // TODO: log error + return + } + + feeBumpTxXDR, err := feeBumpTx.Base64() + if err != nil { + // TODO: log error + return + } + + err = p.store.UpsertTry(payload.TransactionHash, feeBumpTxHash, feeBumpTxXDR, tss.NewCode) + if err != nil { + // TODO: log error + return + } + rpcSendResp, err := p.trySendTransaction(feeBumpTxXDR) + if err != nil { + // reset the status of the transaction back to NEW so it can be re-processed again + err = p.store.UpsertTransaction(payload.ClientID, payload.TransactionHash, payload.TransactionXDR, tss.NewStatus) + if err != nil { + // TODO: log error + } + // TODO: log error + return + } + + err = p.processRPCSendTxResponse(payload, rpcSendResp) + if err != nil { + // reset the status of the transaction back to NEW so it can be re-processed again + err = p.store.UpsertTransaction(payload.ClientID, payload.TransactionHash, payload.TransactionXDR, tss.NewStatus) + if err != nil { + // TODO: log error + } + // TODO: log error + } +} + +func (p *rpcCallerServicePool) trySendTransaction(feeBumpTxXDR string) (tss.RPCSendTxResponse, error) { + rpcSendResp, err := p.txService.SendTransaction(feeBumpTxXDR) + if err != nil { + return tss.RPCSendTxResponse{}, err + } + return rpcSendResp, nil +} + +func (p *rpcCallerServicePool) processRPCSendTxResponse(payload tss.Payload, resp tss.RPCSendTxResponse) error { + err := p.store.UpsertTry(payload.TransactionHash, resp.TransactionHash, resp.TransactionXDR, resp.Code) + if err != nil { + return err + } + err = p.store.UpsertTransaction(payload.TransactionHash, resp.TransactionHash, resp.TransactionXDR, resp.Status) + if err != nil { + return err + } + payload.RpcSubmitTxResponse = resp + if resp.Status == tss.TryAgainLaterStatus || resp.Status == tss.ErrorStatus { + p.errHandlerService.ProcessPayload(payload) + } + return nil +} + +func (p *rpcCallerServicePool) Stop() { + p.pool.StopAndWait() +} diff --git a/internal/tss/services/rpc_caller_service.go b/internal/tss/services/rpc_caller_service.go new file mode 100644 index 0000000..5cbaaba --- /dev/null +++ b/internal/tss/services/rpc_caller_service.go @@ -0,0 +1,19 @@ +package services + +import ( + "github.com/stellar/wallet-backend/internal/tss" +) + +type rpcCallerService struct { + channel tss.Channel +} + +func NewRPCCallerService(channel tss.Channel) Service { + return &rpcCallerService{ + channel: channel, + } +} + +func (p *rpcCallerService) ProcessPayload(payload tss.Payload) { + p.channel.Send(payload) +} diff --git a/internal/tss/services/types.go b/internal/tss/services/types.go new file mode 100644 index 0000000..f395190 --- /dev/null +++ b/internal/tss/services/types.go @@ -0,0 +1,7 @@ +package services + +import "github.com/stellar/wallet-backend/internal/tss" + +type Service interface { + ProcessPayload(payload tss.Payload) +} diff --git a/internal/tss/store/store.go b/internal/tss/store/store.go new file mode 100644 index 0000000..f1d2696 --- /dev/null +++ b/internal/tss/store/store.go @@ -0,0 +1,49 @@ +package tss_store + +import ( + "context" + "fmt" + + "github.com/stellar/wallet-backend/internal/db" + "github.com/stellar/wallet-backend/internal/tss" +) + +type store struct { + DB db.ConnectionPool + ctx context.Context +} + +func NewStore(ctx context.Context, db db.ConnectionPool) Store { + return &store{ + DB: db, + ctx: ctx, + } +} + +func (s *store) UpsertTransaction(clientID string, txHash string, txXDR string, status tss.RPCTXStatus) error { + const q = ` + INSERT INTO + tss_transactions (transaction_hash, transaction_xdr, webhook_url, current_status) + VALUES + ($1, $2, $3, $4) +` + _, err := s.DB.ExecContext(s.ctx, q, txHash, txXDR, clientID, string(status)) + if err != nil { + return fmt.Errorf("inserting/updatig tss transaction: %w", err) + } + return nil +} + +func (s *store) UpsertTry(txHash string, feeBumpTxHash string, feeBumpTxXDR string, status tss.RPCTXCode) error { + const q = ` + INSERT INTO + tss_transaction_submission_tries (original_transaction_hash, try_transaction_hash, try_transaction_xdr, status) + VALUES + ($1, $2, $3, $4) +` + _, err := s.DB.ExecContext(s.ctx, q, txHash, feeBumpTxHash, feeBumpTxXDR, status) + if err != nil { + return fmt.Errorf("inserting/updating tss try: %w", err) + } + return nil +} diff --git a/internal/tss/store/types.go b/internal/tss/store/types.go new file mode 100644 index 0000000..4f7782d --- /dev/null +++ b/internal/tss/store/types.go @@ -0,0 +1,10 @@ +package tss_store + +import ( + "github.com/stellar/wallet-backend/internal/tss" +) + +type Store interface { + UpsertTransaction(clientID string, txHash string, txXDR string, status tss.RPCTXStatus) error + UpsertTry(transactionHash string, feeBumpTxHash string, feeBumpTxXDR string, status tss.RPCTXCode) error +} diff --git a/internal/tss/channel.go b/internal/tss/types.go similarity index 83% rename from internal/tss/channel.go rename to internal/tss/types.go index 205a485..bdec2bd 100644 --- a/internal/tss/channel.go +++ b/internal/tss/types.go @@ -1,8 +1,15 @@ package tss type RPCTXStatus string +type RPCTXCode string const ( + NewCode RPCTXCode = "NEW" +) + +const ( + // Brand new transaction, not sent to RPC yet + NewStatus RPCTXStatus = "NEW" // RPC sendTransaction statuses PendingStatus RPCTXStatus = "PENDING" DuplicateStatus RPCTXStatus = "DUPLICATE" @@ -26,16 +33,22 @@ type RPCGetIngestTxResponse struct { } type RPCSendTxResponse struct { + // The hash of the transaction submitted to RPC + TransactionHash string + TransactionXDR string // The status of an RPC sendTransaction call. Can be one of [PENDING, DUPLICATE, TRY_AGAIN_LATER, ERROR] Status RPCTXStatus // The (optional) error code that is derived by deserialzing the errorResultXdr string in the sendTransaction response // list of possible errror codes: https://developers.stellar.org/docs/data/horizon/api-reference/errors/result-codes/transactions - ErrorCode string + Code RPCTXCode } type Payload struct { + ClientID string // The hash of the transaction xdr submitted by the client - the id of the transaction submitted by a client TransactionHash string + // The xdr of the transaction + TransactionXDR string // Relevant fields in an RPC sendTransaction response RpcSubmitTxResponse RPCSendTxResponse // Relevant fields in the transaction list inside the RPC getTransactions response @@ -45,4 +58,5 @@ type Payload struct { type Channel interface { Send(payload Payload) Receive(payload Payload) + Stop() } diff --git a/internal/tss/utils/transaction_service.go b/internal/tss/utils/transaction_service.go index 66639d5..19a38b3 100644 --- a/internal/tss/utils/transaction_service.go +++ b/internal/tss/utils/transaction_service.go @@ -31,6 +31,7 @@ type transactionService struct { HorizonClient horizonclient.ClientInterface RpcUrl string BaseFee int64 + Ctx context.Context } type TransactionServiceOptions struct { @@ -39,6 +40,7 @@ type TransactionServiceOptions struct { HorizonClient horizonclient.ClientInterface RpcUrl string BaseFee int64 + Ctx context.Context } func (o *TransactionServiceOptions) ValidateOptions() error { @@ -64,6 +66,20 @@ func (o *TransactionServiceOptions) ValidateOptions() error { return nil } +func NewTransactionService(opts TransactionServiceOptions) (TransactionService, error) { + if err := opts.ValidateOptions(); err != nil { + return nil, err + } + return &transactionService{ + DistributionAccountSignatureClient: opts.DistributionAccountSignatureClient, + ChannelAccountSignatureClient: opts.ChannelAccountSignatureClient, + HorizonClient: opts.HorizonClient, + RpcUrl: opts.RpcUrl, + BaseFee: opts.BaseFee, + Ctx: opts.Ctx, + }, nil +} + func parseJSONBody(body []byte) (map[string]interface{}, error) { var res map[string]interface{} err := json.Unmarshal(body, &res) @@ -108,7 +124,7 @@ func sendRPCRequest(rpcUrl string, method string, params map[string]string) (map return res, nil } -func (t *transactionService) SignAndBuildNewTransaction(ctx context.Context, origTxXdr string) (*txnbuild.FeeBumpTransaction, error) { +func (t *transactionService) SignAndBuildNewTransaction(origTxXdr string) (*txnbuild.FeeBumpTransaction, error) { genericTx, err := txnbuild.TransactionFromXDR(origTxXdr) if err != nil { return nil, tssErr.OriginalXdrMalformed @@ -117,7 +133,7 @@ func (t *transactionService) SignAndBuildNewTransaction(ctx context.Context, ori if !txEmpty { return nil, tssErr.OriginalXdrMalformed } - channelAccountPublicKey, err := t.ChannelAccountSignatureClient.GetAccountPublicKey(ctx) + channelAccountPublicKey, err := t.ChannelAccountSignatureClient.GetAccountPublicKey(t.Ctx) if err != nil { return nil, fmt.Errorf("getting channel account public key: %w", err) } @@ -139,12 +155,12 @@ func (t *transactionService) SignAndBuildNewTransaction(ctx context.Context, ori if err != nil { return nil, fmt.Errorf("building transaction: %w", err) } - tx, err = t.ChannelAccountSignatureClient.SignStellarTransaction(ctx, tx, channelAccountPublicKey) + tx, err = t.ChannelAccountSignatureClient.SignStellarTransaction(t.Ctx, tx, channelAccountPublicKey) if err != nil { return nil, fmt.Errorf("signing transaction with channel account: %w", err) } // wrap the transaction in a fee bump tx, signed by the distribution account - distributionAccountPublicKey, err := t.DistributionAccountSignatureClient.GetAccountPublicKey(ctx) + distributionAccountPublicKey, err := t.DistributionAccountSignatureClient.GetAccountPublicKey(t.Ctx) if err != nil { return nil, fmt.Errorf("getting distribution account public key: %w", err) } @@ -160,7 +176,7 @@ func (t *transactionService) SignAndBuildNewTransaction(ctx context.Context, ori return nil, fmt.Errorf("building fee-bump transaction %w", err) } - feeBumpTx, err = t.DistributionAccountSignatureClient.SignStellarFeeBumpTransaction(ctx, feeBumpTx) + feeBumpTx, err = t.DistributionAccountSignatureClient.SignStellarFeeBumpTransaction(t.Ctx, feeBumpTx) if err != nil { return nil, fmt.Errorf("signing the fee bump transaction with distribution account: %w", err) } @@ -174,6 +190,7 @@ func (t *transactionService) SendTransaction(transactionXdr string) (tss.RPCSend } sendTxResponse := tss.RPCSendTxResponse{} + sendTxResponse.TransactionXDR = transactionXdr if result, ok := rpcResponse["result"].(map[string]interface{}); ok { if val, exists := result["status"].(tss.RPCTXStatus); exists { sendTxResponse.Status = val @@ -183,7 +200,10 @@ func (t *transactionService) SendTransaction(transactionXdr string) (tss.RPCSend if err != nil { return sendTxResponse, fmt.Errorf("SendTransaction: unable to unmarshal errorResultXdr: %s", val) } - sendTxResponse.ErrorCode = errorCode + sendTxResponse.Code = tss.RPCTXCode(errorCode) + } + if hash, exists := result["hash"].(string); exists { + sendTxResponse.TransactionHash = hash } } return sendTxResponse, nil @@ -214,16 +234,3 @@ func (t *transactionService) GetTransaction(transactionHash string) (tss.RPCGetI } return getIngestTxResponse, nil } - -func NewTransactionService(opts TransactionServiceOptions) (*transactionService, error) { - if err := opts.ValidateOptions(); err != nil { - return nil, err - } - return &transactionService{ - DistributionAccountSignatureClient: opts.DistributionAccountSignatureClient, - ChannelAccountSignatureClient: opts.ChannelAccountSignatureClient, - HorizonClient: opts.HorizonClient, - RpcUrl: opts.RpcUrl, - BaseFee: opts.BaseFee, - }, nil -} diff --git a/internal/tss/utils/transaction_service_test.go b/internal/tss/utils/transaction_service_test.go index 3a37cb4..c578367 100644 --- a/internal/tss/utils/transaction_service_test.go +++ b/internal/tss/utils/transaction_service_test.go @@ -482,7 +482,7 @@ func TestSendTransaction(t *testing.T) { rpcSendTxResponse, err := txService.SendTransaction(txXdr) assert.Equal(t, rpcSendTxResponse.Status, tss.ErrorStatus) - assert.Empty(t, rpcSendTxResponse.ErrorCode) + assert.Empty(t, rpcSendTxResponse.Code) assert.Equal(t, "SendTransaction: unable to unmarshal errorResultXdr: "+errorResultXdr, err.Error()) }) t.Run("return_send_tx_response", func(t *testing.T) { @@ -500,7 +500,7 @@ func TestSendTransaction(t *testing.T) { rpcSendTxResponse, err := txService.SendTransaction(txXdr) assert.Equal(t, rpcSendTxResponse.Status, tss.ErrorStatus) - assert.Equal(t, rpcSendTxResponse.ErrorCode, "txError") + assert.Equal(t, rpcSendTxResponse.Code, "txError") assert.Empty(t, err) }) } diff --git a/internal/tss/utils/types.go b/internal/tss/utils/types.go new file mode 100644 index 0000000..f67c8f9 --- /dev/null +++ b/internal/tss/utils/types.go @@ -0,0 +1,12 @@ +package utils + +import ( + "github.com/stellar/go/txnbuild" + "github.com/stellar/wallet-backend/internal/tss" +) + +type TransactionService interface { + SignAndBuildNewTransaction(origTxXdr string) (*txnbuild.FeeBumpTransaction, error) + SendTransaction(transactionXdr string) (tss.RPCSendTxResponse, error) + GetTransaction(transactionHash string) (tss.RPCGetIngestTxResponse, error) +} From 3890bfe749dd1d4d27c458968fcb445f378a5a02 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Thu, 12 Sep 2024 11:52:34 -0700 Subject: [PATCH 022/113] p.txService.NetworkPassPhrase() --- internal/tss/channels/rpc_caller_service_channel.go | 2 +- internal/tss/utils/transaction_service.go | 4 ++++ internal/tss/utils/types.go | 1 + 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/internal/tss/channels/rpc_caller_service_channel.go b/internal/tss/channels/rpc_caller_service_channel.go index 51815a3..c2b4a5e 100644 --- a/internal/tss/channels/rpc_caller_service_channel.go +++ b/internal/tss/channels/rpc_caller_service_channel.go @@ -44,7 +44,7 @@ func (p *rpcCallerServicePool) Receive(payload tss.Payload) { // TODO: log error return } - feeBumpTxHash, err := feeBumpTx.HashHex("need network pass") + feeBumpTxHash, err := feeBumpTx.HashHex(p.txService.NetworkPassPhrase()) if err != nil { // TODO: log error return diff --git a/internal/tss/utils/transaction_service.go b/internal/tss/utils/transaction_service.go index 19a38b3..6789563 100644 --- a/internal/tss/utils/transaction_service.go +++ b/internal/tss/utils/transaction_service.go @@ -124,6 +124,10 @@ func sendRPCRequest(rpcUrl string, method string, params map[string]string) (map return res, nil } +func (t *transactionService) NetworkPassPhrase() string { + return t.DistributionAccountSignatureClient.NetworkPassphrase() +} + func (t *transactionService) SignAndBuildNewTransaction(origTxXdr string) (*txnbuild.FeeBumpTransaction, error) { genericTx, err := txnbuild.TransactionFromXDR(origTxXdr) if err != nil { diff --git a/internal/tss/utils/types.go b/internal/tss/utils/types.go index f67c8f9..1cec932 100644 --- a/internal/tss/utils/types.go +++ b/internal/tss/utils/types.go @@ -6,6 +6,7 @@ import ( ) type TransactionService interface { + NetworkPassPhrase() string SignAndBuildNewTransaction(origTxXdr string) (*txnbuild.FeeBumpTransaction, error) SendTransaction(transactionXdr string) (tss.RPCSendTxResponse, error) GetTransaction(transactionHash string) (tss.RPCGetIngestTxResponse, error) From e87c6d6f6acf7deb29ec4a857b923ae19ed01171 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Fri, 13 Sep 2024 01:34:48 -0700 Subject: [PATCH 023/113] last commit before writing unit tests --- cmd/serve.go | 3 + cmd/utils/global_options.go | 32 ++++++++ .../2024-08-28.0-tss_transactions.sql | 8 +- internal/serve/serve.go | 41 +++++++++++ .../channels/rpc_caller_service_channel.go | 73 +++++++++---------- internal/tss/store/store.go | 33 +++++++-- internal/tss/store/types.go | 4 +- internal/tss/types.go | 20 ++++- internal/tss/utils/transaction_service.go | 24 +++--- .../tss/utils/transaction_service_test.go | 33 +++++---- 10 files changed, 187 insertions(+), 84 deletions(-) diff --git a/cmd/serve.go b/cmd/serve.go index 314b6dd..e37378d 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -26,6 +26,9 @@ func (c *serveCmd) Command() *cobra.Command { utils.NetworkPassphraseOption(&cfg.NetworkPassphrase), utils.BaseFeeOption(&cfg.BaseFee), utils.HorizonClientURLOption(&cfg.HorizonClientURL), + utils.RPCURLOption(&cfg.RpcUrl), + utils.RPCCallerServiceChannelBufferSizeOption(&cfg.RPCCallerServiceChannelBufferSize), + utils.RPCCallerServiceMaxWorkersOption(&cfg.RPCCallerServiceChannelMaxWorkers), utils.ChannelAccountEncryptionPassphraseOption(&cfg.EncryptionPassphrase), { Name: "port", diff --git a/cmd/utils/global_options.go b/cmd/utils/global_options.go index 7603646..eaabf13 100644 --- a/cmd/utils/global_options.go +++ b/cmd/utils/global_options.go @@ -67,6 +67,38 @@ func HorizonClientURLOption(configKey *string) *config.ConfigOption { } } +func RPCURLOption(configKey *string) *config.ConfigOption { + return &config.ConfigOption{ + Name: "rpc-url", + Usage: "The URL of the RPC Server.", + OptType: types.String, + ConfigKey: configKey, + FlagDefault: "localhost:8080", + Required: true, + } +} + +func RPCCallerServiceChannelBufferSizeOption(configKey *int) *config.ConfigOption { + return &config.ConfigOption{ + Name: "tss-rpc-caller-service-channel-buffer-size", + Usage: "Set the buffer size for TSS RPC Caller Service channel.", + OptType: types.Int, + ConfigKey: configKey, + FlagDefault: 1000, + } +} + +func RPCCallerServiceMaxWorkersOption(configKey *int) *config.ConfigOption { + return &config.ConfigOption{ + Name: "tss-rpc-caller-service-channel-max-workers", + Usage: "Set the maximum number of workers for TSS RPC Caller Service channel.", + OptType: types.Int, + ConfigKey: configKey, + FlagDefault: 100, + } + +} + func ChannelAccountEncryptionPassphraseOption(configKey *string) *config.ConfigOption { return &config.ConfigOption{ Name: "channel-account-encryption-passphrase", diff --git a/internal/db/migrations/2024-08-28.0-tss_transactions.sql b/internal/db/migrations/2024-08-28.0-tss_transactions.sql index 0f03c3c..6af0c6f 100644 --- a/internal/db/migrations/2024-08-28.0-tss_transactions.sql +++ b/internal/db/migrations/2024-08-28.0-tss_transactions.sql @@ -11,10 +11,10 @@ CREATE TABLE tss_transactions ( ); CREATE TABLE tss_transaction_submission_tries ( + try_transaction_hash TEXT PRIMARY_KEY, original_transaction_hash TEXT, - try_transaction_hash TEXT, try_transaction_xdr TEXT, - status TEXT, + status INTEGER, updated_at TIMESTAMPTZ DEFAULT NOW() ); @@ -22,8 +22,7 @@ CREATE INDEX idx_tx_current_status ON tss_transactions(current_status); CREATE INDEX idx_claimed_until ON tss_transactions(claimed_until); CREATE INDEX idx_original_transaction_hash ON tss_transaction_submission_tries(original_transaction_hash); -CREATE INDEX idx_try_transaction_hash ON tss_transaction_submission_tries(try_transaction_hash); -CREATE INDEX idx_last_updated ON tss_transaction_submission_tries(last_updated); +CREATE INDEX idx_last_updated ON tss_transaction_submission_tries(updated_at); -- +migrate Down @@ -32,6 +31,5 @@ DROP INDEX IF EXISTS idx_claimed_until; DROP TABLE tss_transactions; DROP INDEX IF EXISTS idx_original_transaction_hash; -DROP INDEX IF EXISTS idx_try_transaction_hash; DROP INDEX IF EXISTS idx_last_updated; DROP TABLE tss_transaction_submission_tries; diff --git a/internal/serve/serve.go b/internal/serve/serve.go index 2d1ab8f..edc2933 100644 --- a/internal/serve/serve.go +++ b/internal/serve/serve.go @@ -24,6 +24,11 @@ import ( "github.com/stellar/wallet-backend/internal/signing" "github.com/stellar/wallet-backend/internal/signing/store" signingutils "github.com/stellar/wallet-backend/internal/signing/utils" + "github.com/stellar/wallet-backend/internal/tss" + tsschannel "github.com/stellar/wallet-backend/internal/tss/channels" + tssservices "github.com/stellar/wallet-backend/internal/tss/services" + tssstore "github.com/stellar/wallet-backend/internal/tss/store" + tssutils "github.com/stellar/wallet-backend/internal/tss/utils" ) // NOTE: perhaps move this to a environment variable. @@ -57,6 +62,10 @@ type Configs struct { HorizonClientURL string DistributionAccountSignatureClient signing.SignatureClient ChannelAccountSignatureClient signing.SignatureClient + // TSS + RpcUrl string + RPCCallerServiceChannelBufferSize int + RPCCallerServiceChannelMaxWorkers int } type handlerDeps struct { @@ -70,6 +79,9 @@ type handlerDeps struct { AccountService services.AccountService AccountSponsorshipService services.AccountSponsorshipService PaymentService services.PaymentService + // TSS + RpcCallerServiceChannel tss.Channel + RpcCallerService tssservices.Service } func Serve(cfg Configs) error { @@ -87,6 +99,7 @@ func Serve(cfg Configs) error { }, OnStopping: func() { log.Info("Stopping Wallet Backend server") + deps.RpcCallerServiceChannel.Stop() }, }) @@ -150,6 +163,32 @@ func initHandlerDeps(cfg Configs) (handlerDeps, error) { } go ensureChannelAccounts(channelAccountService, int64(cfg.NumberOfChannelAccounts)) + // TSS + ctx := context.Background() + txServiceOpts := tssutils.TransactionServiceOptions{ + DistributionAccountSignatureClient: cfg.DistributionAccountSignatureClient, + ChannelAccountSignatureClient: cfg.ChannelAccountSignatureClient, + HorizonClient: &horizonClient, + RpcUrl: cfg.RpcUrl, + BaseFee: int64(cfg.BaseFee), // Reuse horizon base fee for RPC?? + Ctx: ctx, + } + tssTxService, err := tssutils.NewTransactionService(txServiceOpts) + if err != nil { + return handlerDeps{}, fmt.Errorf("instantiating tss transaction service: %w", err) + } + + // re-use same context as above?? + store := tssstore.NewStore(ctx, dbConnectionPool) + tssChannelConfigs := tsschannel.RPCCallerServiceChannelConfigs{ + Store: store, + TxService: tssTxService, + MaxBufferSize: cfg.RPCCallerServiceChannelBufferSize, + MaxWorkers: cfg.RPCCallerServiceChannelMaxWorkers, + } + rpcCallerServiceChannel := tsschannel.NewRPCCallerServiceChannel(tssChannelConfigs) + rpcCallerService := tssservices.NewRPCCallerService(rpcCallerServiceChannel) + return handlerDeps{ Models: models, SignatureVerifier: signatureVerifier, @@ -157,6 +196,8 @@ func initHandlerDeps(cfg Configs) (handlerDeps, error) { AccountService: accountService, AccountSponsorshipService: accountSponsorshipService, PaymentService: paymentService, + RpcCallerServiceChannel: rpcCallerServiceChannel, + RpcCallerService: rpcCallerService, }, nil } diff --git a/internal/tss/channels/rpc_caller_service_channel.go b/internal/tss/channels/rpc_caller_service_channel.go index c2b4a5e..27a6b12 100644 --- a/internal/tss/channels/rpc_caller_service_channel.go +++ b/internal/tss/channels/rpc_caller_service_channel.go @@ -8,6 +8,14 @@ import ( "github.com/stellar/wallet-backend/internal/tss/utils" ) +type RPCCallerServiceChannelConfigs struct { + Store tss_store.Store + TxService utils.TransactionService + // add pool configs here + MaxBufferSize int + MaxWorkers int +} + type rpcCallerServicePool struct { pool *pond.WorkerPool // some pool config, make a config struct for it @@ -16,13 +24,14 @@ type rpcCallerServicePool struct { store tss_store.Store } -func NewRPCCallerServiceChannel(store tss_store.Store, txService utils.TransactionService) (tss.Channel, error) { - pool := pond.New(10, 0, pond.MinWorkers(10)) +func NewRPCCallerServiceChannel(cfg RPCCallerServiceChannelConfigs) tss.Channel { + // use cfg to build pool + pool := pond.New(cfg.MaxBufferSize, cfg.MaxWorkers, pond.Strategy(pond.Balanced())) return &rpcCallerServicePool{ pool: pool, - txService: txService, - store: store, - }, nil + txService: cfg.TxService, + store: cfg.Store, + } } func (p *rpcCallerServicePool) Send(payload tss.Payload) { @@ -32,13 +41,16 @@ func (p *rpcCallerServicePool) Send(payload tss.Payload) { } func (p *rpcCallerServicePool) Receive(payload tss.Payload) { - // maybe: why is sqlsqlExec db.SQLExecuter being passed GetAllByPublicKey in channel_accounts_model.go ? - err := p.store.UpsertTransaction(payload.ClientID, payload.TransactionHash, payload.TransactionXDR, tss.NewStatus) + err := p.store.UpsertTransaction(payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.NewStatus) if err != nil { // TODO: log error return } + /* + The reason we return on each error we encounter is so that the transaction status + stays at NEW, so that it can be picked up for re-processing when this pool is restarted. + */ feeBumpTx, err := p.txService.SignAndBuildNewTransaction(payload.TransactionXDR) if err != nil { // TODO: log error @@ -56,55 +68,36 @@ func (p *rpcCallerServicePool) Receive(payload tss.Payload) { return } - err = p.store.UpsertTry(payload.TransactionHash, feeBumpTxHash, feeBumpTxXDR, tss.NewCode) + err = p.store.UpsertTry(payload.TransactionHash, feeBumpTxHash, feeBumpTxXDR, tss.RPCTXCode{OtherCodes: tss.NewCode}) if err != nil { // TODO: log error return } - rpcSendResp, err := p.trySendTransaction(feeBumpTxXDR) - if err != nil { - // reset the status of the transaction back to NEW so it can be re-processed again - err = p.store.UpsertTransaction(payload.ClientID, payload.TransactionHash, payload.TransactionXDR, tss.NewStatus) - if err != nil { - // TODO: log error - } - // TODO: log error + rpcSendResp, err := p.txService.SendTransaction(feeBumpTxXDR) + + // if the rpc submitTransaction fails, or we cannot unmarshal it's response, we return because we want to retry this transaction + if rpcSendResp.Code.OtherCodes == tss.RPCFailCode || rpcSendResp.Code.OtherCodes == tss.UnMarshalBinaryCode { + // TODO: log here return } - err = p.processRPCSendTxResponse(payload, rpcSendResp) + err = p.store.UpsertTry(payload.TransactionHash, rpcSendResp.TransactionHash, rpcSendResp.TransactionXDR, rpcSendResp.Code) if err != nil { - // reset the status of the transaction back to NEW so it can be re-processed again - err = p.store.UpsertTransaction(payload.ClientID, payload.TransactionHash, payload.TransactionXDR, tss.NewStatus) - if err != nil { - // TODO: log error - } // TODO: log error + return } -} -func (p *rpcCallerServicePool) trySendTransaction(feeBumpTxXDR string) (tss.RPCSendTxResponse, error) { - rpcSendResp, err := p.txService.SendTransaction(feeBumpTxXDR) + err = p.store.UpsertTransaction(payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, rpcSendResp.Status) if err != nil { - return tss.RPCSendTxResponse{}, err + // TODO: log error + return } - return rpcSendResp, nil -} -func (p *rpcCallerServicePool) processRPCSendTxResponse(payload tss.Payload, resp tss.RPCSendTxResponse) error { - err := p.store.UpsertTry(payload.TransactionHash, resp.TransactionHash, resp.TransactionXDR, resp.Code) - if err != nil { - return err - } - err = p.store.UpsertTransaction(payload.TransactionHash, resp.TransactionHash, resp.TransactionXDR, resp.Status) - if err != nil { - return err - } - payload.RpcSubmitTxResponse = resp - if resp.Status == tss.TryAgainLaterStatus || resp.Status == tss.ErrorStatus { + // route the payload to the Error handler service + payload.RpcSubmitTxResponse = rpcSendResp + if rpcSendResp.Status == tss.TryAgainLaterStatus || rpcSendResp.Status == tss.ErrorStatus { p.errHandlerService.ProcessPayload(payload) } - return nil } func (p *rpcCallerServicePool) Stop() { diff --git a/internal/tss/store/store.go b/internal/tss/store/store.go index f1d2696..e6c75a3 100644 --- a/internal/tss/store/store.go +++ b/internal/tss/store/store.go @@ -1,4 +1,4 @@ -package tss_store +package store import ( "context" @@ -20,14 +20,20 @@ func NewStore(ctx context.Context, db db.ConnectionPool) Store { } } -func (s *store) UpsertTransaction(clientID string, txHash string, txXDR string, status tss.RPCTXStatus) error { +func (s *store) UpsertTransaction(webhookURL string, txHash string, txXDR string, status tss.RPCTXStatus) error { const q = ` INSERT INTO tss_transactions (transaction_hash, transaction_xdr, webhook_url, current_status) VALUES ($1, $2, $3, $4) -` - _, err := s.DB.ExecContext(s.ctx, q, txHash, txXDR, clientID, string(status)) + ON CONFLICT (transaction_hash) + DO UPDATE SET + transaction_xdr = $2, + webhook_url = $3, + current_status = $4, + updated_at = NOW(); + ` + _, err := s.DB.ExecContext(s.ctx, q, txHash, txXDR, webhookURL, string(status)) if err != nil { return fmt.Errorf("inserting/updatig tss transaction: %w", err) } @@ -37,11 +43,24 @@ func (s *store) UpsertTransaction(clientID string, txHash string, txXDR string, func (s *store) UpsertTry(txHash string, feeBumpTxHash string, feeBumpTxXDR string, status tss.RPCTXCode) error { const q = ` INSERT INTO - tss_transaction_submission_tries (original_transaction_hash, try_transaction_hash, try_transaction_xdr, status) + tss_transaction_submission_tries (try_transaction_hash, original_transaction_hash, try_transaction_xdr, status) VALUES ($1, $2, $3, $4) -` - _, err := s.DB.ExecContext(s.ctx, q, txHash, feeBumpTxHash, feeBumpTxXDR, status) + ON CONFLICT (try_transaction_hash) + DO UPDATE SET + original_transaction_hash = $1, + try_transaction_xdr = $3, + status = $4, + updated_at = NOW(); + ` + var st int + // if this value is set, it takes precedence over the code from RPC + if status.OtherCodes != tss.NoCode { + st = int(status.OtherCodes) + } else { + st = int(status.TxResultCode) + } + _, err := s.DB.ExecContext(s.ctx, q, txHash, feeBumpTxHash, feeBumpTxXDR, st) if err != nil { return fmt.Errorf("inserting/updating tss try: %w", err) } diff --git a/internal/tss/store/types.go b/internal/tss/store/types.go index 4f7782d..645e081 100644 --- a/internal/tss/store/types.go +++ b/internal/tss/store/types.go @@ -1,10 +1,10 @@ -package tss_store +package store import ( "github.com/stellar/wallet-backend/internal/tss" ) type Store interface { - UpsertTransaction(clientID string, txHash string, txXDR string, status tss.RPCTXStatus) error + UpsertTransaction(WebhookURL string, txHash string, txXDR string, status tss.RPCTXStatus) error UpsertTry(transactionHash string, feeBumpTxHash string, feeBumpTxXDR string, status tss.RPCTXCode) error } diff --git a/internal/tss/types.go b/internal/tss/types.go index bdec2bd..3f055f4 100644 --- a/internal/tss/types.go +++ b/internal/tss/types.go @@ -1,12 +1,26 @@ package tss +import "github.com/stellar/go/xdr" + type RPCTXStatus string -type RPCTXCode string +type OtherCodes int32 + +type TransactionResultCode int32 const ( - NewCode RPCTXCode = "NEW" + // Do not use NoCode + NoCode OtherCodes = 0 + // These values need to not overlap the values in xdr.TransactionResultCode + NewCode OtherCodes = 100 + RPCFailCode OtherCodes = 101 + UnMarshalBinaryCode OtherCodes = 102 ) +type RPCTXCode struct { + TxResultCode xdr.TransactionResultCode + OtherCodes OtherCodes +} + const ( // Brand new transaction, not sent to RPC yet NewStatus RPCTXStatus = "NEW" @@ -44,7 +58,7 @@ type RPCSendTxResponse struct { } type Payload struct { - ClientID string + WebhookURL string // The hash of the transaction xdr submitted by the client - the id of the transaction submitted by a client TransactionHash string // The xdr of the transaction diff --git a/internal/tss/utils/transaction_service.go b/internal/tss/utils/transaction_service.go index 6789563..e38b885 100644 --- a/internal/tss/utils/transaction_service.go +++ b/internal/tss/utils/transaction_service.go @@ -89,13 +89,16 @@ func parseJSONBody(body []byte) (map[string]interface{}, error) { return res, nil } -func parseErrorResultXdr(errorResultXdr string) (string, error) { +func parseErrorResultXdr(errorResultXdr string) (tss.RPCTXCode, error) { errorResult := xdr.TransactionResult{} err := errorResult.UnmarshalBinary([]byte(errorResultXdr)) + if err != nil { - return "", fmt.Errorf("SendTransaction: unable to unmarshal errorResultXdr: %s", errorResultXdr) + return tss.RPCTXCode{OtherCodes: tss.UnMarshalBinaryCode}, fmt.Errorf("SendTransaction: unable to unmarshal errorResultXdr: %s", errorResultXdr) } - return errorResult.Result.Code.String(), nil + return tss.RPCTXCode{ + TxResultCode: errorResult.Result.Code, + }, nil } func sendRPCRequest(rpcUrl string, method string, params map[string]string) (map[string]interface{}, error) { @@ -189,28 +192,25 @@ func (t *transactionService) SignAndBuildNewTransaction(origTxXdr string) (*txnb func (t *transactionService) SendTransaction(transactionXdr string) (tss.RPCSendTxResponse, error) { rpcResponse, err := callRPC(t.RpcUrl, "sendTransaction", map[string]string{"transaction": transactionXdr}) + sendTxResponse := tss.RPCSendTxResponse{} + sendTxResponse.TransactionXDR = transactionXdr if err != nil { - return tss.RPCSendTxResponse{}, fmt.Errorf(err.Error()) + sendTxResponse.Code.OtherCodes = tss.RPCFailCode + return sendTxResponse, fmt.Errorf(err.Error()) } - sendTxResponse := tss.RPCSendTxResponse{} - sendTxResponse.TransactionXDR = transactionXdr if result, ok := rpcResponse["result"].(map[string]interface{}); ok { if val, exists := result["status"].(tss.RPCTXStatus); exists { sendTxResponse.Status = val } if val, exists := result["errorResultXdr"].(string); exists { - errorCode, err := UnMarshalErrorResultXdr(val) - if err != nil { - return sendTxResponse, fmt.Errorf("SendTransaction: unable to unmarshal errorResultXdr: %s", val) - } - sendTxResponse.Code = tss.RPCTXCode(errorCode) + sendTxResponse.Code, err = UnMarshalErrorResultXdr(val) } if hash, exists := result["hash"].(string); exists { sendTxResponse.TransactionHash = hash } } - return sendTxResponse, nil + return sendTxResponse, err } func (t *transactionService) GetTransaction(transactionHash string) (tss.RPCGetIngestTxResponse, error) { diff --git a/internal/tss/utils/transaction_service_test.go b/internal/tss/utils/transaction_service_test.go index c578367..174f2d6 100644 --- a/internal/tss/utils/transaction_service_test.go +++ b/internal/tss/utils/transaction_service_test.go @@ -13,6 +13,7 @@ import ( "github.com/stellar/go/keypair" "github.com/stellar/go/protocols/horizon" "github.com/stellar/go/txnbuild" + "github.com/stellar/go/xdr" "github.com/stellar/wallet-backend/internal/signing" "github.com/stellar/wallet-backend/internal/tss" tssErr "github.com/stellar/wallet-backend/internal/tss/errors" @@ -118,12 +119,13 @@ func TestSignAndBuildNewTransaction(t *testing.T) { HorizonClient: &horizonClient, RpcUrl: "http://localhost:8000/soroban/rpc", BaseFee: 114, + Ctx: context.Background(), }) txStr, _ := buildTestTransaction().Base64() t.Run("malformed_transaction_string", func(t *testing.T) { - feeBumpTx, err := txService.SignAndBuildNewTransaction(context.Background(), "abcd") + feeBumpTx, err := txService.SignAndBuildNewTransaction("abcd") assert.Empty(t, feeBumpTx) assert.ErrorIs(t, tssErr.OriginalXdrMalformed, err) }) @@ -134,7 +136,7 @@ func TestSignAndBuildNewTransaction(t *testing.T) { Return("", errors.New("channel accounts unavailable")). Once() - feeBumpTx, err := txService.SignAndBuildNewTransaction(context.Background(), txStr) + feeBumpTx, err := txService.SignAndBuildNewTransaction(txStr) assert.Empty(t, feeBumpTx) assert.Equal(t, "getting channel account public key: channel accounts unavailable", err.Error()) }) @@ -153,7 +155,7 @@ func TestSignAndBuildNewTransaction(t *testing.T) { Return(horizon.Account{}, errors.New("horizon down")). Once() - feeBumpTx, err := txService.SignAndBuildNewTransaction(context.Background(), txStr) + feeBumpTx, err := txService.SignAndBuildNewTransaction(txStr) assert.Empty(t, feeBumpTx) assert.Equal(t, "getting channel account details from horizon: horizon down", err.Error()) }) @@ -175,7 +177,7 @@ func TestSignAndBuildNewTransaction(t *testing.T) { Return(horizon.Account{AccountID: channelAccount.Address(), Sequence: 1}, nil). Once() - feeBumpTx, err := txService.SignAndBuildNewTransaction(context.Background(), txStr) + feeBumpTx, err := txService.SignAndBuildNewTransaction(txStr) assert.Empty(t, feeBumpTx) assert.Equal(t, "signing transaction with channel account: unable to sign", err.Error()) }) @@ -203,7 +205,7 @@ func TestSignAndBuildNewTransaction(t *testing.T) { Return(horizon.Account{AccountID: channelAccount.Address(), Sequence: 1}, nil). Once() - feeBumpTx, err := txService.SignAndBuildNewTransaction(context.Background(), txStr) + feeBumpTx, err := txService.SignAndBuildNewTransaction(txStr) assert.Empty(t, feeBumpTx) assert.Equal(t, "getting distribution account public key: client down", err.Error()) }) @@ -234,7 +236,7 @@ func TestSignAndBuildNewTransaction(t *testing.T) { Return(horizon.Account{AccountID: account.Address(), Sequence: 1}, nil). Once() - feeBumpTx, err := txService.SignAndBuildNewTransaction(context.Background(), txStr) + feeBumpTx, err := txService.SignAndBuildNewTransaction(txStr) assert.Empty(t, feeBumpTx) assert.Equal(t, "signing the fee bump transaction with distribution account: unable to sign", err.Error()) }) @@ -272,7 +274,7 @@ func TestSignAndBuildNewTransaction(t *testing.T) { Return(horizon.Account{AccountID: account.Address(), Sequence: 1}, nil). Once() - feeBumpTx, err := txService.SignAndBuildNewTransaction(context.Background(), txStr) + feeBumpTx, err := txService.SignAndBuildNewTransaction(txStr) assert.Equal(t, feeBumpTx, testFeeBumpTx) assert.Empty(t, err) }) @@ -436,9 +438,9 @@ type MockUnMarshalErrorResultXdr struct { mock.Mock } -func (m *MockUnMarshalErrorResultXdr) UnMarshalErrorResultXdr(errorResultXdr string) (string, error) { +func (m *MockUnMarshalErrorResultXdr) UnMarshalErrorResultXdr(errorResultXdr string) (tss.RPCTXCode, error) { args := m.Called(errorResultXdr) - return args.String(0), args.Error(1) + return args.Get(0).(tss.RPCTXCode), args.Error(1) } func TestSendTransaction(t *testing.T) { @@ -464,7 +466,8 @@ func TestSendTransaction(t *testing.T) { Return(nil, errors.New("unable to reach rpc server")). Once() - _, err := txService.SendTransaction(txXdr) + rpcSendTxResponse, err := txService.SendTransaction(txXdr) + assert.Equal(t, rpcSendTxResponse.Code.OtherCodes, tss.RPCFailCode) assert.Equal(t, "unable to reach rpc server", err.Error()) }) t.Run("error_unmarshaling_error_result_xdr", func(t *testing.T) { @@ -477,13 +480,13 @@ func TestSendTransaction(t *testing.T) { mockUnMarshalErrorResultXdr. On("UnMarshalErrorResultXdr", errorResultXdr). - Return("", errors.New("unable to unmarshal")). + Return(tss.RPCTXCode{OtherCodes: tss.UnMarshalBinaryCode}, errors.New("unable to unmarshal")). Once() rpcSendTxResponse, err := txService.SendTransaction(txXdr) assert.Equal(t, rpcSendTxResponse.Status, tss.ErrorStatus) - assert.Empty(t, rpcSendTxResponse.Code) - assert.Equal(t, "SendTransaction: unable to unmarshal errorResultXdr: "+errorResultXdr, err.Error()) + assert.Equal(t, rpcSendTxResponse.Code.OtherCodes, tss.UnMarshalBinaryCode) + assert.Equal(t, "unable to unmarshal", err.Error()) }) t.Run("return_send_tx_response", func(t *testing.T) { errorResultXdr := "AAAAAAAAAGT////7AAAAAA==" @@ -495,12 +498,12 @@ func TestSendTransaction(t *testing.T) { mockUnMarshalErrorResultXdr. On("UnMarshalErrorResultXdr", errorResultXdr). - Return("txError", nil). + Return(tss.RPCTXCode{TxResultCode: xdr.TransactionResultCodeTxSuccess}, nil). Once() rpcSendTxResponse, err := txService.SendTransaction(txXdr) assert.Equal(t, rpcSendTxResponse.Status, tss.ErrorStatus) - assert.Equal(t, rpcSendTxResponse.Code, "txError") + assert.Equal(t, rpcSendTxResponse.Code.TxResultCode, xdr.TransactionResultCodeTxSuccess) assert.Empty(t, err) }) } From c1d37868b61554391c105ad2c676a38d92eb3093 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Fri, 13 Sep 2024 01:45:23 -0700 Subject: [PATCH 024/113] Making the transaction service more injectible and adding fields to the Payload --- .../2024-08-28.0-tss_transactions.sql | 8 +-- internal/tss/{channel.go => types.go} | 30 +++++++- internal/tss/utils/transaction_service.go | 69 +++++++++++-------- .../tss/utils/transaction_service_test.go | 33 +++++---- internal/tss/utils/types.go | 13 ++++ 5 files changed, 103 insertions(+), 50 deletions(-) rename internal/tss/{channel.go => types.go} (70%) create mode 100644 internal/tss/utils/types.go diff --git a/internal/db/migrations/2024-08-28.0-tss_transactions.sql b/internal/db/migrations/2024-08-28.0-tss_transactions.sql index 0f03c3c..6af0c6f 100644 --- a/internal/db/migrations/2024-08-28.0-tss_transactions.sql +++ b/internal/db/migrations/2024-08-28.0-tss_transactions.sql @@ -11,10 +11,10 @@ CREATE TABLE tss_transactions ( ); CREATE TABLE tss_transaction_submission_tries ( + try_transaction_hash TEXT PRIMARY_KEY, original_transaction_hash TEXT, - try_transaction_hash TEXT, try_transaction_xdr TEXT, - status TEXT, + status INTEGER, updated_at TIMESTAMPTZ DEFAULT NOW() ); @@ -22,8 +22,7 @@ CREATE INDEX idx_tx_current_status ON tss_transactions(current_status); CREATE INDEX idx_claimed_until ON tss_transactions(claimed_until); CREATE INDEX idx_original_transaction_hash ON tss_transaction_submission_tries(original_transaction_hash); -CREATE INDEX idx_try_transaction_hash ON tss_transaction_submission_tries(try_transaction_hash); -CREATE INDEX idx_last_updated ON tss_transaction_submission_tries(last_updated); +CREATE INDEX idx_last_updated ON tss_transaction_submission_tries(updated_at); -- +migrate Down @@ -32,6 +31,5 @@ DROP INDEX IF EXISTS idx_claimed_until; DROP TABLE tss_transactions; DROP INDEX IF EXISTS idx_original_transaction_hash; -DROP INDEX IF EXISTS idx_try_transaction_hash; DROP INDEX IF EXISTS idx_last_updated; DROP TABLE tss_transaction_submission_tries; diff --git a/internal/tss/channel.go b/internal/tss/types.go similarity index 70% rename from internal/tss/channel.go rename to internal/tss/types.go index 205a485..3f055f4 100644 --- a/internal/tss/channel.go +++ b/internal/tss/types.go @@ -1,8 +1,29 @@ package tss +import "github.com/stellar/go/xdr" + type RPCTXStatus string +type OtherCodes int32 + +type TransactionResultCode int32 + +const ( + // Do not use NoCode + NoCode OtherCodes = 0 + // These values need to not overlap the values in xdr.TransactionResultCode + NewCode OtherCodes = 100 + RPCFailCode OtherCodes = 101 + UnMarshalBinaryCode OtherCodes = 102 +) + +type RPCTXCode struct { + TxResultCode xdr.TransactionResultCode + OtherCodes OtherCodes +} const ( + // Brand new transaction, not sent to RPC yet + NewStatus RPCTXStatus = "NEW" // RPC sendTransaction statuses PendingStatus RPCTXStatus = "PENDING" DuplicateStatus RPCTXStatus = "DUPLICATE" @@ -26,16 +47,22 @@ type RPCGetIngestTxResponse struct { } type RPCSendTxResponse struct { + // The hash of the transaction submitted to RPC + TransactionHash string + TransactionXDR string // The status of an RPC sendTransaction call. Can be one of [PENDING, DUPLICATE, TRY_AGAIN_LATER, ERROR] Status RPCTXStatus // The (optional) error code that is derived by deserialzing the errorResultXdr string in the sendTransaction response // list of possible errror codes: https://developers.stellar.org/docs/data/horizon/api-reference/errors/result-codes/transactions - ErrorCode string + Code RPCTXCode } type Payload struct { + WebhookURL string // The hash of the transaction xdr submitted by the client - the id of the transaction submitted by a client TransactionHash string + // The xdr of the transaction + TransactionXDR string // Relevant fields in an RPC sendTransaction response RpcSubmitTxResponse RPCSendTxResponse // Relevant fields in the transaction list inside the RPC getTransactions response @@ -45,4 +72,5 @@ type Payload struct { type Channel interface { Send(payload Payload) Receive(payload Payload) + Stop() } diff --git a/internal/tss/utils/transaction_service.go b/internal/tss/utils/transaction_service.go index 66639d5..e38b885 100644 --- a/internal/tss/utils/transaction_service.go +++ b/internal/tss/utils/transaction_service.go @@ -31,6 +31,7 @@ type transactionService struct { HorizonClient horizonclient.ClientInterface RpcUrl string BaseFee int64 + Ctx context.Context } type TransactionServiceOptions struct { @@ -39,6 +40,7 @@ type TransactionServiceOptions struct { HorizonClient horizonclient.ClientInterface RpcUrl string BaseFee int64 + Ctx context.Context } func (o *TransactionServiceOptions) ValidateOptions() error { @@ -64,6 +66,20 @@ func (o *TransactionServiceOptions) ValidateOptions() error { return nil } +func NewTransactionService(opts TransactionServiceOptions) (TransactionService, error) { + if err := opts.ValidateOptions(); err != nil { + return nil, err + } + return &transactionService{ + DistributionAccountSignatureClient: opts.DistributionAccountSignatureClient, + ChannelAccountSignatureClient: opts.ChannelAccountSignatureClient, + HorizonClient: opts.HorizonClient, + RpcUrl: opts.RpcUrl, + BaseFee: opts.BaseFee, + Ctx: opts.Ctx, + }, nil +} + func parseJSONBody(body []byte) (map[string]interface{}, error) { var res map[string]interface{} err := json.Unmarshal(body, &res) @@ -73,13 +89,16 @@ func parseJSONBody(body []byte) (map[string]interface{}, error) { return res, nil } -func parseErrorResultXdr(errorResultXdr string) (string, error) { +func parseErrorResultXdr(errorResultXdr string) (tss.RPCTXCode, error) { errorResult := xdr.TransactionResult{} err := errorResult.UnmarshalBinary([]byte(errorResultXdr)) + if err != nil { - return "", fmt.Errorf("SendTransaction: unable to unmarshal errorResultXdr: %s", errorResultXdr) + return tss.RPCTXCode{OtherCodes: tss.UnMarshalBinaryCode}, fmt.Errorf("SendTransaction: unable to unmarshal errorResultXdr: %s", errorResultXdr) } - return errorResult.Result.Code.String(), nil + return tss.RPCTXCode{ + TxResultCode: errorResult.Result.Code, + }, nil } func sendRPCRequest(rpcUrl string, method string, params map[string]string) (map[string]interface{}, error) { @@ -108,7 +127,11 @@ func sendRPCRequest(rpcUrl string, method string, params map[string]string) (map return res, nil } -func (t *transactionService) SignAndBuildNewTransaction(ctx context.Context, origTxXdr string) (*txnbuild.FeeBumpTransaction, error) { +func (t *transactionService) NetworkPassPhrase() string { + return t.DistributionAccountSignatureClient.NetworkPassphrase() +} + +func (t *transactionService) SignAndBuildNewTransaction(origTxXdr string) (*txnbuild.FeeBumpTransaction, error) { genericTx, err := txnbuild.TransactionFromXDR(origTxXdr) if err != nil { return nil, tssErr.OriginalXdrMalformed @@ -117,7 +140,7 @@ func (t *transactionService) SignAndBuildNewTransaction(ctx context.Context, ori if !txEmpty { return nil, tssErr.OriginalXdrMalformed } - channelAccountPublicKey, err := t.ChannelAccountSignatureClient.GetAccountPublicKey(ctx) + channelAccountPublicKey, err := t.ChannelAccountSignatureClient.GetAccountPublicKey(t.Ctx) if err != nil { return nil, fmt.Errorf("getting channel account public key: %w", err) } @@ -139,12 +162,12 @@ func (t *transactionService) SignAndBuildNewTransaction(ctx context.Context, ori if err != nil { return nil, fmt.Errorf("building transaction: %w", err) } - tx, err = t.ChannelAccountSignatureClient.SignStellarTransaction(ctx, tx, channelAccountPublicKey) + tx, err = t.ChannelAccountSignatureClient.SignStellarTransaction(t.Ctx, tx, channelAccountPublicKey) if err != nil { return nil, fmt.Errorf("signing transaction with channel account: %w", err) } // wrap the transaction in a fee bump tx, signed by the distribution account - distributionAccountPublicKey, err := t.DistributionAccountSignatureClient.GetAccountPublicKey(ctx) + distributionAccountPublicKey, err := t.DistributionAccountSignatureClient.GetAccountPublicKey(t.Ctx) if err != nil { return nil, fmt.Errorf("getting distribution account public key: %w", err) } @@ -160,7 +183,7 @@ func (t *transactionService) SignAndBuildNewTransaction(ctx context.Context, ori return nil, fmt.Errorf("building fee-bump transaction %w", err) } - feeBumpTx, err = t.DistributionAccountSignatureClient.SignStellarFeeBumpTransaction(ctx, feeBumpTx) + feeBumpTx, err = t.DistributionAccountSignatureClient.SignStellarFeeBumpTransaction(t.Ctx, feeBumpTx) if err != nil { return nil, fmt.Errorf("signing the fee bump transaction with distribution account: %w", err) } @@ -169,24 +192,25 @@ func (t *transactionService) SignAndBuildNewTransaction(ctx context.Context, ori func (t *transactionService) SendTransaction(transactionXdr string) (tss.RPCSendTxResponse, error) { rpcResponse, err := callRPC(t.RpcUrl, "sendTransaction", map[string]string{"transaction": transactionXdr}) + sendTxResponse := tss.RPCSendTxResponse{} + sendTxResponse.TransactionXDR = transactionXdr if err != nil { - return tss.RPCSendTxResponse{}, fmt.Errorf(err.Error()) + sendTxResponse.Code.OtherCodes = tss.RPCFailCode + return sendTxResponse, fmt.Errorf(err.Error()) } - sendTxResponse := tss.RPCSendTxResponse{} if result, ok := rpcResponse["result"].(map[string]interface{}); ok { if val, exists := result["status"].(tss.RPCTXStatus); exists { sendTxResponse.Status = val } if val, exists := result["errorResultXdr"].(string); exists { - errorCode, err := UnMarshalErrorResultXdr(val) - if err != nil { - return sendTxResponse, fmt.Errorf("SendTransaction: unable to unmarshal errorResultXdr: %s", val) - } - sendTxResponse.ErrorCode = errorCode + sendTxResponse.Code, err = UnMarshalErrorResultXdr(val) + } + if hash, exists := result["hash"].(string); exists { + sendTxResponse.TransactionHash = hash } } - return sendTxResponse, nil + return sendTxResponse, err } func (t *transactionService) GetTransaction(transactionHash string) (tss.RPCGetIngestTxResponse, error) { @@ -214,16 +238,3 @@ func (t *transactionService) GetTransaction(transactionHash string) (tss.RPCGetI } return getIngestTxResponse, nil } - -func NewTransactionService(opts TransactionServiceOptions) (*transactionService, error) { - if err := opts.ValidateOptions(); err != nil { - return nil, err - } - return &transactionService{ - DistributionAccountSignatureClient: opts.DistributionAccountSignatureClient, - ChannelAccountSignatureClient: opts.ChannelAccountSignatureClient, - HorizonClient: opts.HorizonClient, - RpcUrl: opts.RpcUrl, - BaseFee: opts.BaseFee, - }, nil -} diff --git a/internal/tss/utils/transaction_service_test.go b/internal/tss/utils/transaction_service_test.go index 3a37cb4..174f2d6 100644 --- a/internal/tss/utils/transaction_service_test.go +++ b/internal/tss/utils/transaction_service_test.go @@ -13,6 +13,7 @@ import ( "github.com/stellar/go/keypair" "github.com/stellar/go/protocols/horizon" "github.com/stellar/go/txnbuild" + "github.com/stellar/go/xdr" "github.com/stellar/wallet-backend/internal/signing" "github.com/stellar/wallet-backend/internal/tss" tssErr "github.com/stellar/wallet-backend/internal/tss/errors" @@ -118,12 +119,13 @@ func TestSignAndBuildNewTransaction(t *testing.T) { HorizonClient: &horizonClient, RpcUrl: "http://localhost:8000/soroban/rpc", BaseFee: 114, + Ctx: context.Background(), }) txStr, _ := buildTestTransaction().Base64() t.Run("malformed_transaction_string", func(t *testing.T) { - feeBumpTx, err := txService.SignAndBuildNewTransaction(context.Background(), "abcd") + feeBumpTx, err := txService.SignAndBuildNewTransaction("abcd") assert.Empty(t, feeBumpTx) assert.ErrorIs(t, tssErr.OriginalXdrMalformed, err) }) @@ -134,7 +136,7 @@ func TestSignAndBuildNewTransaction(t *testing.T) { Return("", errors.New("channel accounts unavailable")). Once() - feeBumpTx, err := txService.SignAndBuildNewTransaction(context.Background(), txStr) + feeBumpTx, err := txService.SignAndBuildNewTransaction(txStr) assert.Empty(t, feeBumpTx) assert.Equal(t, "getting channel account public key: channel accounts unavailable", err.Error()) }) @@ -153,7 +155,7 @@ func TestSignAndBuildNewTransaction(t *testing.T) { Return(horizon.Account{}, errors.New("horizon down")). Once() - feeBumpTx, err := txService.SignAndBuildNewTransaction(context.Background(), txStr) + feeBumpTx, err := txService.SignAndBuildNewTransaction(txStr) assert.Empty(t, feeBumpTx) assert.Equal(t, "getting channel account details from horizon: horizon down", err.Error()) }) @@ -175,7 +177,7 @@ func TestSignAndBuildNewTransaction(t *testing.T) { Return(horizon.Account{AccountID: channelAccount.Address(), Sequence: 1}, nil). Once() - feeBumpTx, err := txService.SignAndBuildNewTransaction(context.Background(), txStr) + feeBumpTx, err := txService.SignAndBuildNewTransaction(txStr) assert.Empty(t, feeBumpTx) assert.Equal(t, "signing transaction with channel account: unable to sign", err.Error()) }) @@ -203,7 +205,7 @@ func TestSignAndBuildNewTransaction(t *testing.T) { Return(horizon.Account{AccountID: channelAccount.Address(), Sequence: 1}, nil). Once() - feeBumpTx, err := txService.SignAndBuildNewTransaction(context.Background(), txStr) + feeBumpTx, err := txService.SignAndBuildNewTransaction(txStr) assert.Empty(t, feeBumpTx) assert.Equal(t, "getting distribution account public key: client down", err.Error()) }) @@ -234,7 +236,7 @@ func TestSignAndBuildNewTransaction(t *testing.T) { Return(horizon.Account{AccountID: account.Address(), Sequence: 1}, nil). Once() - feeBumpTx, err := txService.SignAndBuildNewTransaction(context.Background(), txStr) + feeBumpTx, err := txService.SignAndBuildNewTransaction(txStr) assert.Empty(t, feeBumpTx) assert.Equal(t, "signing the fee bump transaction with distribution account: unable to sign", err.Error()) }) @@ -272,7 +274,7 @@ func TestSignAndBuildNewTransaction(t *testing.T) { Return(horizon.Account{AccountID: account.Address(), Sequence: 1}, nil). Once() - feeBumpTx, err := txService.SignAndBuildNewTransaction(context.Background(), txStr) + feeBumpTx, err := txService.SignAndBuildNewTransaction(txStr) assert.Equal(t, feeBumpTx, testFeeBumpTx) assert.Empty(t, err) }) @@ -436,9 +438,9 @@ type MockUnMarshalErrorResultXdr struct { mock.Mock } -func (m *MockUnMarshalErrorResultXdr) UnMarshalErrorResultXdr(errorResultXdr string) (string, error) { +func (m *MockUnMarshalErrorResultXdr) UnMarshalErrorResultXdr(errorResultXdr string) (tss.RPCTXCode, error) { args := m.Called(errorResultXdr) - return args.String(0), args.Error(1) + return args.Get(0).(tss.RPCTXCode), args.Error(1) } func TestSendTransaction(t *testing.T) { @@ -464,7 +466,8 @@ func TestSendTransaction(t *testing.T) { Return(nil, errors.New("unable to reach rpc server")). Once() - _, err := txService.SendTransaction(txXdr) + rpcSendTxResponse, err := txService.SendTransaction(txXdr) + assert.Equal(t, rpcSendTxResponse.Code.OtherCodes, tss.RPCFailCode) assert.Equal(t, "unable to reach rpc server", err.Error()) }) t.Run("error_unmarshaling_error_result_xdr", func(t *testing.T) { @@ -477,13 +480,13 @@ func TestSendTransaction(t *testing.T) { mockUnMarshalErrorResultXdr. On("UnMarshalErrorResultXdr", errorResultXdr). - Return("", errors.New("unable to unmarshal")). + Return(tss.RPCTXCode{OtherCodes: tss.UnMarshalBinaryCode}, errors.New("unable to unmarshal")). Once() rpcSendTxResponse, err := txService.SendTransaction(txXdr) assert.Equal(t, rpcSendTxResponse.Status, tss.ErrorStatus) - assert.Empty(t, rpcSendTxResponse.ErrorCode) - assert.Equal(t, "SendTransaction: unable to unmarshal errorResultXdr: "+errorResultXdr, err.Error()) + assert.Equal(t, rpcSendTxResponse.Code.OtherCodes, tss.UnMarshalBinaryCode) + assert.Equal(t, "unable to unmarshal", err.Error()) }) t.Run("return_send_tx_response", func(t *testing.T) { errorResultXdr := "AAAAAAAAAGT////7AAAAAA==" @@ -495,12 +498,12 @@ func TestSendTransaction(t *testing.T) { mockUnMarshalErrorResultXdr. On("UnMarshalErrorResultXdr", errorResultXdr). - Return("txError", nil). + Return(tss.RPCTXCode{TxResultCode: xdr.TransactionResultCodeTxSuccess}, nil). Once() rpcSendTxResponse, err := txService.SendTransaction(txXdr) assert.Equal(t, rpcSendTxResponse.Status, tss.ErrorStatus) - assert.Equal(t, rpcSendTxResponse.ErrorCode, "txError") + assert.Equal(t, rpcSendTxResponse.Code.TxResultCode, xdr.TransactionResultCodeTxSuccess) assert.Empty(t, err) }) } diff --git a/internal/tss/utils/types.go b/internal/tss/utils/types.go new file mode 100644 index 0000000..1cec932 --- /dev/null +++ b/internal/tss/utils/types.go @@ -0,0 +1,13 @@ +package utils + +import ( + "github.com/stellar/go/txnbuild" + "github.com/stellar/wallet-backend/internal/tss" +) + +type TransactionService interface { + NetworkPassPhrase() string + SignAndBuildNewTransaction(origTxXdr string) (*txnbuild.FeeBumpTransaction, error) + SendTransaction(transactionXdr string) (tss.RPCSendTxResponse, error) + GetTransaction(transactionHash string) (tss.RPCGetIngestTxResponse, error) +} From 01ed3eb5ca1bd158de744f017c82ceb759e24b89 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Fri, 13 Sep 2024 01:52:00 -0700 Subject: [PATCH 025/113] typo --- internal/db/migrations/2024-08-28.0-tss_transactions.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/db/migrations/2024-08-28.0-tss_transactions.sql b/internal/db/migrations/2024-08-28.0-tss_transactions.sql index 6af0c6f..5653ccb 100644 --- a/internal/db/migrations/2024-08-28.0-tss_transactions.sql +++ b/internal/db/migrations/2024-08-28.0-tss_transactions.sql @@ -1,7 +1,7 @@ -- +migrate Up CREATE TABLE tss_transactions ( - transaction_hash TEXT PRIMARY KEY, + transaction_hash TEXT PRIMARY_KEY, transaction_xdr TEXT, webhook_url TEXT, current_status TEXT, From 3ddd2bd0367d33c21f1917b32d3e6b8c7f338f7f Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Fri, 13 Sep 2024 01:55:06 -0700 Subject: [PATCH 026/113] typo --- internal/db/migrations/2024-08-28.0-tss_transactions.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/db/migrations/2024-08-28.0-tss_transactions.sql b/internal/db/migrations/2024-08-28.0-tss_transactions.sql index 5653ccb..e40b6a9 100644 --- a/internal/db/migrations/2024-08-28.0-tss_transactions.sql +++ b/internal/db/migrations/2024-08-28.0-tss_transactions.sql @@ -1,7 +1,7 @@ -- +migrate Up CREATE TABLE tss_transactions ( - transaction_hash TEXT PRIMARY_KEY, + transaction_hash TEXT PRIMARY KEY, transaction_xdr TEXT, webhook_url TEXT, current_status TEXT, @@ -11,7 +11,7 @@ CREATE TABLE tss_transactions ( ); CREATE TABLE tss_transaction_submission_tries ( - try_transaction_hash TEXT PRIMARY_KEY, + try_transaction_hash TEXT PRIMARY KEY, original_transaction_hash TEXT, try_transaction_xdr TEXT, status INTEGER, From 5776cb1b5411de73b38923372fcc6eaeddfb8823 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Fri, 13 Sep 2024 02:27:46 -0700 Subject: [PATCH 027/113] Update 2024-08-28.0-tss_transactions.sql --- internal/db/migrations/2024-08-28.0-tss_transactions.sql | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/db/migrations/2024-08-28.0-tss_transactions.sql b/internal/db/migrations/2024-08-28.0-tss_transactions.sql index 6af0c6f..e40b6a9 100644 --- a/internal/db/migrations/2024-08-28.0-tss_transactions.sql +++ b/internal/db/migrations/2024-08-28.0-tss_transactions.sql @@ -11,7 +11,7 @@ CREATE TABLE tss_transactions ( ); CREATE TABLE tss_transaction_submission_tries ( - try_transaction_hash TEXT PRIMARY_KEY, + try_transaction_hash TEXT PRIMARY KEY, original_transaction_hash TEXT, try_transaction_xdr TEXT, status INTEGER, From 77a6e4b5314e45e0bd48deebc9433589b8441faa Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Fri, 13 Sep 2024 11:18:00 -0700 Subject: [PATCH 028/113] lint errors --- internal/tss/utils/transaction_builder.go | 5 ++++- internal/tss/utils/transaction_builder_test.go | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/internal/tss/utils/transaction_builder.go b/internal/tss/utils/transaction_builder.go index ea1702c..8347200 100644 --- a/internal/tss/utils/transaction_builder.go +++ b/internal/tss/utils/transaction_builder.go @@ -20,7 +20,10 @@ func BuildOriginalTransaction(txOpXDRs []string) (*txnbuild.Transaction, error) } dec := xdr3.NewDecoder(strings.NewReader(string(decodedBytes))) var decodedOp xdr.Operation - dec.Decode(&decodedOp) + _, err = dec.Decode(&decodedOp) + if err != nil { + return nil, fmt.Errorf("decoding xdr into xdr Operation: %w", err) + } // for now, we assume that all operations are Payment operations paymentOp := txnbuild.Payment{} err = paymentOp.FromXDR(decodedOp) diff --git a/internal/tss/utils/transaction_builder_test.go b/internal/tss/utils/transaction_builder_test.go index a0df8fa..0d8897f 100644 --- a/internal/tss/utils/transaction_builder_test.go +++ b/internal/tss/utils/transaction_builder_test.go @@ -27,7 +27,7 @@ func TestBuildOriginalTransaction(t *testing.T) { buf := strings.Builder{} enc := xdr3.NewEncoder(&buf) - caOp.EncodeTo(enc) + _ = caOp.EncodeTo(enc) caOpXDR := buf.String() caOpXDRBase64 := base64.StdEncoding.EncodeToString([]byte(caOpXDR)) @@ -48,7 +48,7 @@ func TestBuildOriginalTransaction(t *testing.T) { var buf strings.Builder enc := xdr3.NewEncoder(&buf) - op.EncodeTo(enc) + _ = op.EncodeTo(enc) opXDR := buf.String() opXDRBase64 := base64.StdEncoding.EncodeToString([]byte(opXDR)) From 01109ab87f44d99441b1f9010526ddec19a83590 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Fri, 13 Sep 2024 11:19:44 -0700 Subject: [PATCH 029/113] go mod tidy --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index 66fd755..52a892c 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/spf13/cobra v1.8.0 github.com/spf13/viper v1.17.0 github.com/stellar/go v0.0.0-20240416222646-fd107948e6c4 + github.com/stellar/go-xdr v0.0.0-20231122183749-b53fb00bcac2 github.com/stretchr/testify v1.9.0 golang.org/x/term v0.18.0 ) @@ -75,7 +76,6 @@ require ( github.com/spf13/afero v1.10.0 // indirect github.com/spf13/cast v1.5.1 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/stellar/go-xdr v0.0.0-20231122183749-b53fb00bcac2 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect go.opencensus.io v0.24.0 // indirect From 38ba21b85ede3c3b40a73ef3ef265cf07938d968 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Sun, 15 Sep 2024 15:46:22 -0700 Subject: [PATCH 030/113] test cases + other changes --- go.mod | 2 +- internal/serve/serve.go | 10 +- internal/tss/channels/mocks.go | 15 ++ .../channels/rpc_caller_service_channel.go | 55 +++--- .../rpc_caller_service_channel_test.go | 184 ++++++++++++++++++ internal/tss/channels/types.go | 6 + .../tss/services/error_handler_service.go | 21 ++ internal/tss/services/mocks.go | 16 ++ internal/tss/store/mocks.go | 22 +++ internal/tss/store/store.go | 2 +- internal/tss/store/store_test.go | 96 +++++++++ internal/tss/utils/helpers.go | 39 ++++ internal/tss/utils/mocks.go | 37 ++++ .../tss/utils/transaction_service_test.go | 32 +-- 14 files changed, 476 insertions(+), 61 deletions(-) create mode 100644 internal/tss/channels/mocks.go create mode 100644 internal/tss/channels/rpc_caller_service_channel_test.go create mode 100644 internal/tss/channels/types.go create mode 100644 internal/tss/services/error_handler_service.go create mode 100644 internal/tss/services/mocks.go create mode 100644 internal/tss/store/mocks.go create mode 100644 internal/tss/store/store_test.go create mode 100644 internal/tss/utils/helpers.go create mode 100644 internal/tss/utils/mocks.go diff --git a/go.mod b/go.mod index 9e73829..063511e 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/stellar/wallet-backend go 1.22.0 require ( + github.com/alitto/pond v1.9.2 github.com/aws/aws-sdk-go v1.45.26 github.com/getsentry/sentry-go v0.28.1 github.com/go-chi/chi v4.1.2+incompatible @@ -29,7 +30,6 @@ require ( github.com/BurntSushi/toml v1.3.2 // indirect github.com/Masterminds/squirrel v1.5.4 // indirect github.com/Microsoft/go-winio v0.6.1 // indirect - github.com/alitto/pond v1.9.2 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v4 v4.2.1 // indirect diff --git a/internal/serve/serve.go b/internal/serve/serve.go index ffc3719..37c10ee 100644 --- a/internal/serve/serve.go +++ b/internal/serve/serve.go @@ -184,11 +184,13 @@ func initHandlerDeps(cfg Configs) (handlerDeps, error) { // re-use same context as above?? store := tssstore.NewStore(ctx, dbConnectionPool) + errorHandlerService := tssservices.NewErrorHandlerService(nil) tssChannelConfigs := tsschannel.RPCCallerServiceChannelConfigs{ - Store: store, - TxService: tssTxService, - MaxBufferSize: cfg.RPCCallerServiceChannelBufferSize, - MaxWorkers: cfg.RPCCallerServiceChannelMaxWorkers, + Store: store, + TxService: tssTxService, + ErrHandlerService: errorHandlerService, + MaxBufferSize: cfg.RPCCallerServiceChannelBufferSize, + MaxWorkers: cfg.RPCCallerServiceChannelMaxWorkers, } rpcCallerServiceChannel := tsschannel.NewRPCCallerServiceChannel(tssChannelConfigs) rpcCallerService := tssservices.NewRPCCallerService(rpcCallerServiceChannel) diff --git a/internal/tss/channels/mocks.go b/internal/tss/channels/mocks.go new file mode 100644 index 0000000..c2bda63 --- /dev/null +++ b/internal/tss/channels/mocks.go @@ -0,0 +1,15 @@ +package channels + +import "github.com/stretchr/testify/mock" + +type WorkerPoolMock struct { + mock.Mock +} + +func (m *WorkerPoolMock) Submit(task func()) { + m.Called(task) +} + +func (m *WorkerPoolMock) Stop() { + m.Called() +} diff --git a/internal/tss/channels/rpc_caller_service_channel.go b/internal/tss/channels/rpc_caller_service_channel.go index 27a6b12..233b4d1 100644 --- a/internal/tss/channels/rpc_caller_service_channel.go +++ b/internal/tss/channels/rpc_caller_service_channel.go @@ -2,6 +2,7 @@ package channels import ( "github.com/alitto/pond" + "github.com/stellar/go/support/log" "github.com/stellar/wallet-backend/internal/tss" tss_services "github.com/stellar/wallet-backend/internal/tss/services" tss_store "github.com/stellar/wallet-backend/internal/tss/store" @@ -9,28 +10,27 @@ import ( ) type RPCCallerServiceChannelConfigs struct { - Store tss_store.Store - TxService utils.TransactionService - // add pool configs here - MaxBufferSize int - MaxWorkers int + Store tss_store.Store + TxService utils.TransactionService + ErrHandlerService tss_services.Service + MaxBufferSize int + MaxWorkers int } type rpcCallerServicePool struct { - pool *pond.WorkerPool - // some pool config, make a config struct for it + pool WorkerPool txService utils.TransactionService errHandlerService tss_services.Service store tss_store.Store } func NewRPCCallerServiceChannel(cfg RPCCallerServiceChannelConfigs) tss.Channel { - // use cfg to build pool pool := pond.New(cfg.MaxBufferSize, cfg.MaxWorkers, pond.Strategy(pond.Balanced())) return &rpcCallerServicePool{ - pool: pool, - txService: cfg.TxService, - store: cfg.Store, + pool: pool, + txService: cfg.TxService, + errHandlerService: cfg.ErrHandlerService, + store: cfg.Store, } } @@ -41,55 +41,54 @@ func (p *rpcCallerServicePool) Send(payload tss.Payload) { } func (p *rpcCallerServicePool) Receive(payload tss.Payload) { + err := p.store.UpsertTransaction(payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.NewStatus) if err != nil { - // TODO: log error + log.Errorf("RPCCallerService: Unable to upsert transaction into transactions table: %s", err.Error()) return } - /* The reason we return on each error we encounter is so that the transaction status - stays at NEW, so that it can be picked up for re-processing when this pool is restarted. + stays at NEW, so that it does not progress any further and + can be picked up for re-processing when this pool is restarted. */ feeBumpTx, err := p.txService.SignAndBuildNewTransaction(payload.TransactionXDR) if err != nil { - // TODO: log error + log.Errorf("RPCCallerService: Unable to sign/build transaction: %s", err.Error()) return } feeBumpTxHash, err := feeBumpTx.HashHex(p.txService.NetworkPassPhrase()) if err != nil { - // TODO: log error + log.Errorf("RPCCallerService: Unable to hashhex fee bump transaction: %s", err.Error()) return } feeBumpTxXDR, err := feeBumpTx.Base64() if err != nil { - // TODO: log error + log.Errorf("RPCCallerService: Unable to base64 fee bump transaction: %s", err.Error()) return } - err = p.store.UpsertTry(payload.TransactionHash, feeBumpTxHash, feeBumpTxXDR, tss.RPCTXCode{OtherCodes: tss.NewCode}) if err != nil { - // TODO: log error + log.Errorf("RPCCallerService: Unable to upsert try in tries table: %s", err.Error()) return } - rpcSendResp, err := p.txService.SendTransaction(feeBumpTxXDR) + rpcSendResp, rpcErr := p.txService.SendTransaction(feeBumpTxXDR) - // if the rpc submitTransaction fails, or we cannot unmarshal it's response, we return because we want to retry this transaction - if rpcSendResp.Code.OtherCodes == tss.RPCFailCode || rpcSendResp.Code.OtherCodes == tss.UnMarshalBinaryCode { - // TODO: log here + err = p.store.UpsertTry(payload.TransactionHash, feeBumpTxHash, feeBumpTxXDR, rpcSendResp.Code) + if err != nil { + log.Errorf("RPCCallerService: Unable to upsert try in tries table: %s", err.Error()) return } - - err = p.store.UpsertTry(payload.TransactionHash, rpcSendResp.TransactionHash, rpcSendResp.TransactionXDR, rpcSendResp.Code) - if err != nil { - // TODO: log error + // if the rpc submitTransaction fails, or we cannot unmarshal it's response, we return because we want to retry this transaction + if rpcErr != nil && rpcSendResp.Code.OtherCodes == tss.RPCFailCode || rpcSendResp.Code.OtherCodes == tss.UnMarshalBinaryCode { + log.Errorf("RPCCallerService: RPC fail: %s", rpcErr.Error()) return } err = p.store.UpsertTransaction(payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, rpcSendResp.Status) if err != nil { - // TODO: log error + log.Errorf("RPCCallerService:Unable to do the final update of tx in the transactions table: %s", err.Error()) return } diff --git a/internal/tss/channels/rpc_caller_service_channel_test.go b/internal/tss/channels/rpc_caller_service_channel_test.go new file mode 100644 index 0000000..3a19742 --- /dev/null +++ b/internal/tss/channels/rpc_caller_service_channel_test.go @@ -0,0 +1,184 @@ +package channels + +import ( + "context" + "errors" + "testing" + + "github.com/stellar/go/xdr" + "github.com/stellar/wallet-backend/internal/db" + "github.com/stellar/wallet-backend/internal/db/dbtest" + "github.com/stellar/wallet-backend/internal/tss" + tss_services "github.com/stellar/wallet-backend/internal/tss/services" + "github.com/stellar/wallet-backend/internal/tss/store" + "github.com/stellar/wallet-backend/internal/tss/utils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestSend(t *testing.T) { + dbt := dbtest.Open(t) + defer dbt.Close() + dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) + require.NoError(t, err) + defer dbConnectionPool.Close() + store := store.NewStore(context.Background(), dbConnectionPool) + txServiceMock := utils.TransactionServiceMock{} + cfgs := RPCCallerServiceChannelConfigs{ + Store: store, + TxService: &txServiceMock, + MaxBufferSize: 10, + MaxWorkers: 10, + } + channel := NewRPCCallerServiceChannel(cfgs) + payload := tss.Payload{} + payload.WebhookURL = "www.stellar.com" + payload.TransactionHash = "hash" + payload.TransactionXDR = "xdr" + txServiceMock. + On("SignAndBuildNewTransaction", payload.TransactionXDR). + Return(nil, errors.New("signing failed")) + channel.Send(payload) + channel.Stop() + + var status string + err = dbConnectionPool.GetContext(context.Background(), &status, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) + require.NoError(t, err) + assert.Equal(t, status, string(tss.NewStatus)) +} + +func TestReceive(t *testing.T) { + dbt := dbtest.Open(t) + defer dbt.Close() + + dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) + require.NoError(t, err) + defer dbConnectionPool.Close() + store := store.NewStore(context.Background(), dbConnectionPool) + txServiceMock := utils.TransactionServiceMock{} + errHandlerService := tss_services.MockService{} + cfgs := RPCCallerServiceChannelConfigs{ + Store: store, + TxService: &txServiceMock, + ErrHandlerService: &errHandlerService, + MaxBufferSize: 1, + MaxWorkers: 1, + } + channel := NewRPCCallerServiceChannel(cfgs) + feeBumpTx := utils.BuildTestFeeBumpTransaction() + networkPass := "passphrase" + payload := tss.Payload{} + payload.WebhookURL = "www.stellar.com" + payload.TransactionHash = "hash" + payload.TransactionXDR = "xdr" + + t.Run("fail_on_tx_build_and_sign", func(t *testing.T) { + txServiceMock. + On("SignAndBuildNewTransaction", payload.TransactionXDR). + Return(nil, errors.New("signing failed")). + Once() + channel.Receive(payload) + + var status string + err = dbConnectionPool.GetContext(context.Background(), &status, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) + require.NoError(t, err) + assert.Equal(t, string(tss.NewStatus), status) + }) + + t.Run("rpc_call_fail", func(t *testing.T) { + txXDR, _ := feeBumpTx.Base64() + sendResp := tss.RPCSendTxResponse{} + sendResp.Code.OtherCodes = tss.RPCFailCode + txServiceMock. + On("SignAndBuildNewTransaction", payload.TransactionXDR). + Return(feeBumpTx, nil). + Once(). + On("NetworkPassPhrase"). + Return(networkPass). + Once(). + On("SendTransaction", txXDR). + Return(sendResp, errors.New("RPC Fail")). + Once() + + channel.Receive(payload) + + var txStatus string + err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) + require.NoError(t, err) + assert.Equal(t, txStatus, string(tss.NewStatus)) + + var tryStatus int + feeBumpTxHash, _ := feeBumpTx.HashHex(networkPass) + err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) + require.NoError(t, err) + assert.Equal(t, int(tss.RPCFailCode), tryStatus) + + }) + + t.Run("rpc_resp_unmarshaling_error", func(t *testing.T) { + txXDR, _ := feeBumpTx.Base64() + sendResp := tss.RPCSendTxResponse{} + sendResp.Code.OtherCodes = tss.UnMarshalBinaryCode + txServiceMock. + On("SignAndBuildNewTransaction", payload.TransactionXDR). + Return(feeBumpTx, nil). + Once(). + On("NetworkPassPhrase"). + Return(networkPass). + Once(). + On("SendTransaction", txXDR). + Return(sendResp, errors.New("unable to unmarshal")). + Once() + + channel.Receive(payload) + + var txStatus string + err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) + require.NoError(t, err) + assert.Equal(t, txStatus, string(tss.NewStatus)) + + var tryStatus int + feeBumpTxHash, _ := feeBumpTx.HashHex(networkPass) + err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) + require.NoError(t, err) + assert.Equal(t, int(tss.UnMarshalBinaryCode), tryStatus) + }) + + t.Run("rpc_returns_error_response", func(t *testing.T) { + feeBumpTxXDR, _ := feeBumpTx.Base64() + feeBumpTxHash, _ := feeBumpTx.HashHex(networkPass) + sendResp := tss.RPCSendTxResponse{} + sendResp.Status = tss.ErrorStatus + sendResp.TransactionHash = feeBumpTxHash + sendResp.TransactionXDR = feeBumpTxXDR + sendResp.Code.TxResultCode = xdr.TransactionResultCodeTxTooEarly + txServiceMock. + On("SignAndBuildNewTransaction", payload.TransactionXDR). + Return(feeBumpTx, nil). + Once(). + On("NetworkPassPhrase"). + Return(networkPass). + Once(). + On("SendTransaction", feeBumpTxXDR). + Return(sendResp, nil). + Once() + errHandlerService. + On("ProcessPayload", mock.AnythingOfType("tss.Payload")). + Return(). + Once() + + channel.Receive(payload) + + var txStatus string + err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) + require.NoError(t, err) + assert.Equal(t, string(tss.ErrorStatus), txStatus) + + var tryStatus int + err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) + require.NoError(t, err) + assert.Equal(t, int(xdr.TransactionResultCodeTxTooEarly), tryStatus) + }) + +} diff --git a/internal/tss/channels/types.go b/internal/tss/channels/types.go new file mode 100644 index 0000000..65ab5e9 --- /dev/null +++ b/internal/tss/channels/types.go @@ -0,0 +1,6 @@ +package channels + +type WorkerPool interface { + Submit(task func()) + StopAndWait() +} diff --git a/internal/tss/services/error_handler_service.go b/internal/tss/services/error_handler_service.go new file mode 100644 index 0000000..78c9546 --- /dev/null +++ b/internal/tss/services/error_handler_service.go @@ -0,0 +1,21 @@ +package services + +import ( + "github.com/stellar/wallet-backend/internal/tss" +) + +// nolint:unused +type errorHandlerService struct { + channel tss.Channel +} + +func NewErrorHandlerService(channel tss.Channel) Service { + return &rpcCallerService{ + channel: channel, + } +} + +// nolint:unused +func (p *errorHandlerService) ProcessPayload(payload tss.Payload) { + // fill in later +} diff --git a/internal/tss/services/mocks.go b/internal/tss/services/mocks.go new file mode 100644 index 0000000..fff8db8 --- /dev/null +++ b/internal/tss/services/mocks.go @@ -0,0 +1,16 @@ +package services + +import ( + "github.com/stellar/wallet-backend/internal/tss" + "github.com/stretchr/testify/mock" +) + +type MockService struct { + mock.Mock +} + +var _ Service = (*MockService)(nil) + +func (s *MockService) ProcessPayload(payload tss.Payload) { + s.Called(payload) +} diff --git a/internal/tss/store/mocks.go b/internal/tss/store/mocks.go new file mode 100644 index 0000000..32fdc14 --- /dev/null +++ b/internal/tss/store/mocks.go @@ -0,0 +1,22 @@ +package store + +import ( + "github.com/stellar/wallet-backend/internal/tss" + "github.com/stretchr/testify/mock" +) + +type MockStore struct { + mock.Mock +} + +var _ Store = (*MockStore)(nil) + +func (s *MockStore) UpsertTransaction(webhookURL string, txHash string, txXDR string, status tss.RPCTXStatus) error { + args := s.Called(webhookURL, txHash, txXDR, status) + return args.Error(0) +} + +func (s *MockStore) UpsertTry(txHash string, feeBumpTxHash string, feeBumpTxXDR string, status tss.RPCTXCode) error { + args := s.Called(txHash, feeBumpTxHash, feeBumpTxXDR, status) + return args.Error(0) +} diff --git a/internal/tss/store/store.go b/internal/tss/store/store.go index e6c75a3..7daac66 100644 --- a/internal/tss/store/store.go +++ b/internal/tss/store/store.go @@ -43,7 +43,7 @@ func (s *store) UpsertTransaction(webhookURL string, txHash string, txXDR string func (s *store) UpsertTry(txHash string, feeBumpTxHash string, feeBumpTxXDR string, status tss.RPCTXCode) error { const q = ` INSERT INTO - tss_transaction_submission_tries (try_transaction_hash, original_transaction_hash, try_transaction_xdr, status) + tss_transaction_submission_tries (original_transaction_hash, try_transaction_hash, try_transaction_xdr, status) VALUES ($1, $2, $3, $4) ON CONFLICT (try_transaction_hash) diff --git a/internal/tss/store/store_test.go b/internal/tss/store/store_test.go new file mode 100644 index 0000000..c813f5b --- /dev/null +++ b/internal/tss/store/store_test.go @@ -0,0 +1,96 @@ +package store + +import ( + "context" + "testing" + + "github.com/stellar/go/xdr" + "github.com/stellar/wallet-backend/internal/db" + "github.com/stellar/wallet-backend/internal/db/dbtest" + "github.com/stellar/wallet-backend/internal/tss" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestUpsertTransaction(t *testing.T) { + dbt := dbtest.Open(t) + defer dbt.Close() + dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) + require.NoError(t, err) + defer dbConnectionPool.Close() + store := NewStore(context.Background(), dbConnectionPool) + t.Run("insert", func(t *testing.T) { + _ = store.UpsertTransaction("www.stellar.org", "hash", "xdr", tss.NewStatus) + + var status string + err = dbConnectionPool.GetContext(context.Background(), &status, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, "hash") + require.NoError(t, err) + assert.Equal(t, status, string(tss.NewStatus)) + }) + + t.Run("update", func(t *testing.T) { + _ = store.UpsertTransaction("www.stellar.org", "hash", "xdr", tss.NewStatus) + _ = store.UpsertTransaction("www.stellar.org", "hash", "xdr", tss.SuccessStatus) + + var status string + err = dbConnectionPool.GetContext(context.Background(), &status, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, "hash") + require.NoError(t, err) + assert.Equal(t, status, string(tss.SuccessStatus)) + + var numRows int + err = dbConnectionPool.GetContext(context.Background(), &numRows, `SELECT count(*) FROM tss_transactions WHERE transaction_hash = $1`, "hash") + require.NoError(t, err) + assert.Equal(t, numRows, 1) + + }) +} + +func TestUpsertTry(t *testing.T) { + dbt := dbtest.Open(t) + defer dbt.Close() + dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) + require.NoError(t, err) + defer dbConnectionPool.Close() + store := NewStore(context.Background(), dbConnectionPool) + t.Run("insert", func(t *testing.T) { + code := tss.RPCTXCode{OtherCodes: tss.NewCode} + _ = store.UpsertTry("hash", "feebumptxhash", "feebumptxxdr", code) + + var status int + err = dbConnectionPool.GetContext(context.Background(), &status, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, "feebumptxhash") + require.NoError(t, err) + assert.Equal(t, status, int(tss.NewCode)) + }) + + t.Run("update_other_code", func(t *testing.T) { + code := tss.RPCTXCode{OtherCodes: tss.NewCode} + _ = store.UpsertTry("hash", "feebumptxhash", "feebumptxxdr", code) + code = tss.RPCTXCode{OtherCodes: tss.RPCFailCode} + _ = store.UpsertTry("hash", "feebumptxhash", "feebumptxxdr", code) + var status int + err = dbConnectionPool.GetContext(context.Background(), &status, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, "feebumptxhash") + require.NoError(t, err) + assert.Equal(t, status, int(tss.RPCFailCode)) + + var numRows int + err = dbConnectionPool.GetContext(context.Background(), &numRows, `SELECT count(*) FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, "feebumptxhash") + require.NoError(t, err) + assert.Equal(t, numRows, 1) + }) + + t.Run("update_tx_code", func(t *testing.T) { + code := tss.RPCTXCode{OtherCodes: tss.NewCode} + _ = store.UpsertTry("hash", "feebumptxhash", "feebumptxxdr", code) + code = tss.RPCTXCode{TxResultCode: xdr.TransactionResultCodeTxSuccess} + _ = store.UpsertTry("hash", "feebumptxhash", "feebumptxxdr", code) + var status int + err = dbConnectionPool.GetContext(context.Background(), &status, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, "feebumptxhash") + require.NoError(t, err) + assert.Equal(t, status, int(xdr.TransactionResultCodeTxSuccess)) + + var numRows int + err = dbConnectionPool.GetContext(context.Background(), &numRows, `SELECT count(*) FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, "feebumptxhash") + require.NoError(t, err) + assert.Equal(t, numRows, 1) + }) +} diff --git a/internal/tss/utils/helpers.go b/internal/tss/utils/helpers.go new file mode 100644 index 0000000..c844a03 --- /dev/null +++ b/internal/tss/utils/helpers.go @@ -0,0 +1,39 @@ +package utils + +import ( + "github.com/stellar/go/keypair" + "github.com/stellar/go/txnbuild" +) + +func BuildTestTransaction() *txnbuild.Transaction { + accountToSponsor := keypair.MustRandom() + + tx, _ := txnbuild.NewTransaction(txnbuild.TransactionParams{ + SourceAccount: &txnbuild.SimpleAccount{ + AccountID: accountToSponsor.Address(), + Sequence: 124, + }, + IncrementSequenceNum: true, + Operations: []txnbuild.Operation{ + &txnbuild.Payment{ + Destination: keypair.MustRandom().Address(), + Amount: "14", + Asset: txnbuild.NativeAsset{}, + }, + }, + BaseFee: 104, + Preconditions: txnbuild.Preconditions{TimeBounds: txnbuild.NewTimeout(10)}, + }) + return tx +} + +func BuildTestFeeBumpTransaction() *txnbuild.FeeBumpTransaction { + + feeBumpTx, _ := txnbuild.NewFeeBumpTransaction( + txnbuild.FeeBumpTransactionParams{ + Inner: BuildTestTransaction(), + FeeAccount: keypair.MustRandom().Address(), + BaseFee: 110, + }) + return feeBumpTx +} diff --git a/internal/tss/utils/mocks.go b/internal/tss/utils/mocks.go new file mode 100644 index 0000000..2a95cdf --- /dev/null +++ b/internal/tss/utils/mocks.go @@ -0,0 +1,37 @@ +package utils + +import ( + "github.com/stellar/go/txnbuild" + "github.com/stellar/wallet-backend/internal/tss" + "github.com/stretchr/testify/mock" +) + +type TransactionServiceMock struct { + mock.Mock +} + +var _ TransactionService = (*TransactionServiceMock)(nil) + +func (t *TransactionServiceMock) NetworkPassPhrase() string { + args := t.Called() + return args.String(0) +} + +func (t *TransactionServiceMock) SignAndBuildNewTransaction(origTxXdr string) (*txnbuild.FeeBumpTransaction, error) { + args := t.Called(origTxXdr) + if result := args.Get(0); result != nil { + return result.(*txnbuild.FeeBumpTransaction), args.Error(1) + } + return nil, args.Error(1) + +} + +func (t *TransactionServiceMock) SendTransaction(transactionXdr string) (tss.RPCSendTxResponse, error) { + args := t.Called(transactionXdr) + return args.Get(0).(tss.RPCSendTxResponse), args.Error(1) +} + +func (t *TransactionServiceMock) GetTransaction(transactionHash string) (tss.RPCGetIngestTxResponse, error) { + args := t.Called(transactionHash) + return args.Get(0).(tss.RPCGetIngestTxResponse), args.Error(1) +} diff --git a/internal/tss/utils/transaction_service_test.go b/internal/tss/utils/transaction_service_test.go index 174f2d6..95060ba 100644 --- a/internal/tss/utils/transaction_service_test.go +++ b/internal/tss/utils/transaction_service_test.go @@ -21,28 +21,6 @@ import ( "github.com/stretchr/testify/mock" ) -func buildTestTransaction() *txnbuild.Transaction { - accountToSponsor := keypair.MustRandom() - - tx, _ := txnbuild.NewTransaction(txnbuild.TransactionParams{ - SourceAccount: &txnbuild.SimpleAccount{ - AccountID: accountToSponsor.Address(), - Sequence: 124, - }, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - &txnbuild.Payment{ - Destination: keypair.MustRandom().Address(), - Amount: "14", - Asset: txnbuild.NativeAsset{}, - }, - }, - BaseFee: 104, - Preconditions: txnbuild.Preconditions{TimeBounds: txnbuild.NewTimeout(10)}, - }) - return tx -} - func TestValidateOptions(t *testing.T) { t.Run("return_error_when_distribution_signature_client_null", func(t *testing.T) { opts := TransactionServiceOptions{ @@ -122,7 +100,7 @@ func TestSignAndBuildNewTransaction(t *testing.T) { Ctx: context.Background(), }) - txStr, _ := buildTestTransaction().Base64() + txStr, _ := BuildTestTransaction().Base64() t.Run("malformed_transaction_string", func(t *testing.T) { feeBumpTx, err := txService.SignAndBuildNewTransaction("abcd") @@ -212,7 +190,7 @@ func TestSignAndBuildNewTransaction(t *testing.T) { t.Run("horizon_client_sign_stellar_transaction_w_distribition_account_err", func(t *testing.T) { account := keypair.MustRandom() - signedTx := buildTestTransaction() + signedTx := BuildTestTransaction() channelAccountSignatureClient. On("GetAccountPublicKey", context.Background()). Return(account.Address(), nil). @@ -243,7 +221,7 @@ func TestSignAndBuildNewTransaction(t *testing.T) { t.Run("returns_signed_tx", func(t *testing.T) { account := keypair.MustRandom() - signedTx := buildTestTransaction() + signedTx := BuildTestTransaction() testFeeBumpTx, _ := txnbuild.NewFeeBumpTransaction( txnbuild.FeeBumpTransactionParams{ Inner: signedTx, @@ -457,7 +435,7 @@ func TestSendTransaction(t *testing.T) { RpcUrl: "http://localhost:8000/soroban/rpc", BaseFee: 114, }) - txXdr, _ := buildTestTransaction().Base64() + txXdr, _ := BuildTestTransaction().Base64() rpcUrl := "http://localhost:8000/soroban/rpc" t.Run("call_rpc_returns_error", func(t *testing.T) { @@ -519,7 +497,7 @@ func TestGetTransaction(t *testing.T) { RpcUrl: "http://localhost:8000/soroban/rpc", BaseFee: 114, }) - txHash, _ := buildTestTransaction().HashHex("abcd") + txHash, _ := BuildTestTransaction().HashHex("abcd") rpcUrl := "http://localhost:8000/soroban/rpc" t.Run("call_rpc_returns_error", func(t *testing.T) { From 8ddb96153ef3d89791b611a85ecd835d54360f8a Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Sun, 15 Sep 2024 16:00:07 -0700 Subject: [PATCH 031/113] remoce unused mocks --- internal/tss/channels/mocks.go | 15 --------------- internal/tss/store/mocks.go | 22 ---------------------- 2 files changed, 37 deletions(-) delete mode 100644 internal/tss/channels/mocks.go delete mode 100644 internal/tss/store/mocks.go diff --git a/internal/tss/channels/mocks.go b/internal/tss/channels/mocks.go deleted file mode 100644 index c2bda63..0000000 --- a/internal/tss/channels/mocks.go +++ /dev/null @@ -1,15 +0,0 @@ -package channels - -import "github.com/stretchr/testify/mock" - -type WorkerPoolMock struct { - mock.Mock -} - -func (m *WorkerPoolMock) Submit(task func()) { - m.Called(task) -} - -func (m *WorkerPoolMock) Stop() { - m.Called() -} diff --git a/internal/tss/store/mocks.go b/internal/tss/store/mocks.go deleted file mode 100644 index 32fdc14..0000000 --- a/internal/tss/store/mocks.go +++ /dev/null @@ -1,22 +0,0 @@ -package store - -import ( - "github.com/stellar/wallet-backend/internal/tss" - "github.com/stretchr/testify/mock" -) - -type MockStore struct { - mock.Mock -} - -var _ Store = (*MockStore)(nil) - -func (s *MockStore) UpsertTransaction(webhookURL string, txHash string, txXDR string, status tss.RPCTXStatus) error { - args := s.Called(webhookURL, txHash, txXDR, status) - return args.Error(0) -} - -func (s *MockStore) UpsertTry(txHash string, feeBumpTxHash string, feeBumpTxXDR string, status tss.RPCTXCode) error { - args := s.Called(txHash, feeBumpTxHash, feeBumpTxXDR, status) - return args.Error(0) -} From d32adb1563257a0062653b3ea6145cdb8080fef0 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Sun, 15 Sep 2024 16:05:37 -0700 Subject: [PATCH 032/113] error handler service returns errorHandlerService --- internal/tss/services/error_handler_service.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/internal/tss/services/error_handler_service.go b/internal/tss/services/error_handler_service.go index 78c9546..a9458b7 100644 --- a/internal/tss/services/error_handler_service.go +++ b/internal/tss/services/error_handler_service.go @@ -4,18 +4,16 @@ import ( "github.com/stellar/wallet-backend/internal/tss" ) -// nolint:unused type errorHandlerService struct { channel tss.Channel } func NewErrorHandlerService(channel tss.Channel) Service { - return &rpcCallerService{ + return &errorHandlerService{ channel: channel, } } -// nolint:unused func (p *errorHandlerService) ProcessPayload(payload tss.Payload) { // fill in later } From e0727b53732674e4a759df82e98ad52b07c5819d Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Wed, 18 Sep 2024 00:25:45 -0700 Subject: [PATCH 033/113] changes based on comments --- internal/tss/errors/errors.go | 2 +- internal/tss/utils/mocks.go | 17 + internal/tss/utils/transaction_service.go | 168 +++--- .../tss/utils/transaction_service_test.go | 526 ++++++++++-------- internal/tss/utils/types.go | 13 - 5 files changed, 403 insertions(+), 323 deletions(-) create mode 100644 internal/tss/utils/mocks.go delete mode 100644 internal/tss/utils/types.go diff --git a/internal/tss/errors/errors.go b/internal/tss/errors/errors.go index 8cb3bc9..69f8bc3 100644 --- a/internal/tss/errors/errors.go +++ b/internal/tss/errors/errors.go @@ -5,5 +5,5 @@ import ( ) var ( - OriginalXdrMalformed = errors.New("transaction string is malformed") + OriginalXDRMalformed = errors.New("transaction string is malformed") ) diff --git a/internal/tss/utils/mocks.go b/internal/tss/utils/mocks.go new file mode 100644 index 0000000..83d59e6 --- /dev/null +++ b/internal/tss/utils/mocks.go @@ -0,0 +1,17 @@ +package utils + +import ( + "io" + "net/http" + + "github.com/stretchr/testify/mock" +) + +type MockHTTPClient struct { + mock.Mock +} + +func (s *MockHTTPClient) Post(url, contentType string, body io.Reader) (resp *http.Response, err error) { + args := s.Called(url, contentType, body) + return args.Get(0).(*http.Response), args.Error(1) +} diff --git a/internal/tss/utils/transaction_service.go b/internal/tss/utils/transaction_service.go index e38b885..720f8bd 100644 --- a/internal/tss/utils/transaction_service.go +++ b/internal/tss/utils/transaction_service.go @@ -3,44 +3,53 @@ package utils import ( "bytes" "context" + "encoding/base64" "encoding/json" "fmt" "io" "net/http" "strconv" + "strings" + xdr3 "github.com/stellar/go-xdr/xdr3" "github.com/stellar/go/clients/horizonclient" "github.com/stellar/go/txnbuild" "github.com/stellar/go/xdr" "github.com/stellar/wallet-backend/internal/signing" "github.com/stellar/wallet-backend/internal/tss" - tssErr "github.com/stellar/wallet-backend/internal/tss/errors" + tsserror "github.com/stellar/wallet-backend/internal/tss/errors" ) -var ( - RpcPost = http.Post - UnMarshalRPCResponse = io.ReadAll - UnMarshalJSON = parseJSONBody - callRPC = sendRPCRequest - UnMarshalErrorResultXdr = parseErrorResultXdr -) +type HTTPClient interface { + Post(url string, t string, body io.Reader) (resp *http.Response, err error) +} + +type TransactionService interface { + NetworkPassphrase() string + SignAndBuildNewFeeBumpTransaction(ctx context.Context, origTxXdr string) (*txnbuild.FeeBumpTransaction, error) + SendTransaction(transactionXdr string) (tss.RPCSendTxResponse, error) + GetTransaction(transactionHash string) (tss.RPCGetIngestTxResponse, error) +} type transactionService struct { DistributionAccountSignatureClient signing.SignatureClient ChannelAccountSignatureClient signing.SignatureClient HorizonClient horizonclient.ClientInterface - RpcUrl string + RPCURL string BaseFee int64 + HTTPClient HTTPClient Ctx context.Context } +var _ TransactionService = (*transactionService)(nil) + type TransactionServiceOptions struct { DistributionAccountSignatureClient signing.SignatureClient ChannelAccountSignatureClient signing.SignatureClient HorizonClient horizonclient.ClientInterface - RpcUrl string + RPCURL string BaseFee int64 - Ctx context.Context + HTTPClient HTTPClient } func (o *TransactionServiceOptions) ValidateOptions() error { @@ -56,17 +65,22 @@ func (o *TransactionServiceOptions) ValidateOptions() error { return fmt.Errorf("horizon client cannot be nil") } - if o.RpcUrl == "" { + if o.RPCURL == "" { return fmt.Errorf("rpc url cannot be empty") } if o.BaseFee < int64(txnbuild.MinBaseFee) { return fmt.Errorf("base fee is lower than the minimum network fee") } + + if o.HTTPClient == nil { + return fmt.Errorf("http client cannot be nil") + } + return nil } -func NewTransactionService(opts TransactionServiceOptions) (TransactionService, error) { +func NewTransactionService(opts TransactionServiceOptions) (*transactionService, error) { if err := opts.ValidateOptions(); err != nil { return nil, err } @@ -74,73 +88,57 @@ func NewTransactionService(opts TransactionServiceOptions) (TransactionService, DistributionAccountSignatureClient: opts.DistributionAccountSignatureClient, ChannelAccountSignatureClient: opts.ChannelAccountSignatureClient, HorizonClient: opts.HorizonClient, - RpcUrl: opts.RpcUrl, + RPCURL: opts.RPCURL, BaseFee: opts.BaseFee, - Ctx: opts.Ctx, + HTTPClient: opts.HTTPClient, }, nil } -func parseJSONBody(body []byte) (map[string]interface{}, error) { - var res map[string]interface{} - err := json.Unmarshal(body, &res) - if err != nil { - return nil, fmt.Errorf(err.Error()) - } - return res, nil -} - -func parseErrorResultXdr(errorResultXdr string) (tss.RPCTXCode, error) { - errorResult := xdr.TransactionResult{} - err := errorResult.UnmarshalBinary([]byte(errorResultXdr)) - - if err != nil { - return tss.RPCTXCode{OtherCodes: tss.UnMarshalBinaryCode}, fmt.Errorf("SendTransaction: unable to unmarshal errorResultXdr: %s", errorResultXdr) - } - return tss.RPCTXCode{ - TxResultCode: errorResult.Result.Code, - }, nil -} - -func sendRPCRequest(rpcUrl string, method string, params map[string]string) (map[string]interface{}, error) { +func (t *transactionService) sendRPCRequest(method string, params map[string]string) (map[string]interface{}, error) { payload := map[string]interface{}{ "jsonrpc": "2.0", "id": 1, "method": method, "params": params, } - jsonData, _ := json.Marshal(payload) + jsonData, err := json.Marshal(payload) + + if err != nil { + return nil, fmt.Errorf("marshaling payload") + } - resp, err := RpcPost(rpcUrl, "application/json", bytes.NewBuffer(jsonData)) + resp, err := t.HTTPClient.Post(t.RPCURL, "application/json", bytes.NewBuffer(jsonData)) if err != nil { - return nil, fmt.Errorf(method+": sending POST request to rpc: %v", err) + return nil, fmt.Errorf("%s: sending POST request to rpc: %v", method, err) } defer resp.Body.Close() - body, err := UnMarshalRPCResponse(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf(method+": unmarshalling rpc response: %v", err) + return nil, fmt.Errorf("%s: unmarshaling RPC response", method) } - res, err := UnMarshalJSON(body) + var res map[string]interface{} + err = json.Unmarshal(body, &res) if err != nil { - return nil, fmt.Errorf(method+": parsing rpc response JSON: %v", err) + return nil, fmt.Errorf("%s: parsing RPC response JSON", method) } return res, nil } -func (t *transactionService) NetworkPassPhrase() string { +func (t *transactionService) NetworkPassphrase() string { return t.DistributionAccountSignatureClient.NetworkPassphrase() } -func (t *transactionService) SignAndBuildNewTransaction(origTxXdr string) (*txnbuild.FeeBumpTransaction, error) { +func (t *transactionService) SignAndBuildNewFeeBumpTransaction(ctx context.Context, origTxXdr string) (*txnbuild.FeeBumpTransaction, error) { genericTx, err := txnbuild.TransactionFromXDR(origTxXdr) if err != nil { - return nil, tssErr.OriginalXdrMalformed + return nil, tsserror.OriginalXDRMalformed } originalTx, txEmpty := genericTx.Transaction() if !txEmpty { - return nil, tssErr.OriginalXdrMalformed + return nil, tsserror.OriginalXDRMalformed } - channelAccountPublicKey, err := t.ChannelAccountSignatureClient.GetAccountPublicKey(t.Ctx) + channelAccountPublicKey, err := t.ChannelAccountSignatureClient.GetAccountPublicKey(ctx) if err != nil { return nil, fmt.Errorf("getting channel account public key: %w", err) } @@ -162,12 +160,12 @@ func (t *transactionService) SignAndBuildNewTransaction(origTxXdr string) (*txnb if err != nil { return nil, fmt.Errorf("building transaction: %w", err) } - tx, err = t.ChannelAccountSignatureClient.SignStellarTransaction(t.Ctx, tx, channelAccountPublicKey) + tx, err = t.ChannelAccountSignatureClient.SignStellarTransaction(ctx, tx, channelAccountPublicKey) if err != nil { return nil, fmt.Errorf("signing transaction with channel account: %w", err) } - // wrap the transaction in a fee bump tx, signed by the distribution account - distributionAccountPublicKey, err := t.DistributionAccountSignatureClient.GetAccountPublicKey(t.Ctx) + // Wrap the transaction in a fee bump tx, signed by the distribution account + distributionAccountPublicKey, err := t.DistributionAccountSignatureClient.GetAccountPublicKey(ctx) if err != nil { return nil, fmt.Errorf("getting distribution account public key: %w", err) } @@ -183,15 +181,37 @@ func (t *transactionService) SignAndBuildNewTransaction(origTxXdr string) (*txnb return nil, fmt.Errorf("building fee-bump transaction %w", err) } - feeBumpTx, err = t.DistributionAccountSignatureClient.SignStellarFeeBumpTransaction(t.Ctx, feeBumpTx) + feeBumpTx, err = t.DistributionAccountSignatureClient.SignStellarFeeBumpTransaction(ctx, feeBumpTx) if err != nil { return nil, fmt.Errorf("signing the fee bump transaction with distribution account: %w", err) } return feeBumpTx, nil } +func (t *transactionService) parseErrorResultXDR(errorResultXdr string) (tss.RPCTXCode, error) { + + //errorResult := xdr.TransactionResult{} + unMarshallErr := "unable to unmarshal errorResultXdr: %s" + //err := errorResult.UnmarshalBinary([]byte(errorResultXdr)) + + decodedBytes, err := base64.StdEncoding.DecodeString(errorResultXdr) + if err != nil { + return tss.RPCTXCode{OtherCodes: tss.UnMarshalBinaryCode}, fmt.Errorf(unMarshallErr, errorResultXdr) + } + dec := xdr3.NewDecoder(strings.NewReader(string(decodedBytes))) + var errorResult xdr.TransactionResult + _, err = dec.Decode(&errorResult) + + if err != nil { + return tss.RPCTXCode{OtherCodes: tss.UnMarshalBinaryCode}, fmt.Errorf(unMarshallErr, errorResultXdr) + } + return tss.RPCTXCode{ + TxResultCode: errorResult.Result.Code, + }, nil +} + func (t *transactionService) SendTransaction(transactionXdr string) (tss.RPCSendTxResponse, error) { - rpcResponse, err := callRPC(t.RpcUrl, "sendTransaction", map[string]string{"transaction": transactionXdr}) + rpcResponse, err := t.sendRPCRequest("sendTransaction", map[string]string{"transaction": transactionXdr}) sendTxResponse := tss.RPCSendTxResponse{} sendTxResponse.TransactionXDR = transactionXdr if err != nil { @@ -200,41 +220,53 @@ func (t *transactionService) SendTransaction(transactionXdr string) (tss.RPCSend } if result, ok := rpcResponse["result"].(map[string]interface{}); ok { - if val, exists := result["status"].(tss.RPCTXStatus); exists { - sendTxResponse.Status = val + if val, exists := result["status"].(string); exists { + sendTxResponse.Status = tss.RPCTXStatus(val) } if val, exists := result["errorResultXdr"].(string); exists { - sendTxResponse.Code, err = UnMarshalErrorResultXdr(val) + sendTxResponse.Code, err = t.parseErrorResultXDR(val) } if hash, exists := result["hash"].(string); exists { sendTxResponse.TransactionHash = hash } + } else { + sendTxResponse.Code.OtherCodes = tss.RPCFailCode + return sendTxResponse, fmt.Errorf("RPC response has no result field") } return sendTxResponse, err } func (t *transactionService) GetTransaction(transactionHash string) (tss.RPCGetIngestTxResponse, error) { - rpcResponse, err := callRPC(t.RpcUrl, "getTransaction", map[string]string{"hash": transactionHash}) + rpcResponse, err := t.sendRPCRequest("getTransaction", map[string]string{"hash": transactionHash}) if err != nil { - return tss.RPCGetIngestTxResponse{}, fmt.Errorf(err.Error()) + return tss.RPCGetIngestTxResponse{Status: tss.ErrorStatus}, fmt.Errorf(err.Error()) } getIngestTxResponse := tss.RPCGetIngestTxResponse{} if result, ok := rpcResponse["result"].(map[string]interface{}); ok { - if status, exists := result["status"].(tss.RPCTXStatus); exists { - getIngestTxResponse.Status = status + if status, exists := result["status"].(string); exists { + getIngestTxResponse.Status = tss.RPCTXStatus(status) } - if envelopeXdr, exists := result["envelopeXdr"].(string); exists { - getIngestTxResponse.EnvelopeXDR = envelopeXdr + if envelopeXDR, exists := result["envelopeXdr"].(string); exists { + getIngestTxResponse.EnvelopeXDR = envelopeXDR } - if resultXdr, exists := result["resultXdr"].(string); exists { - getIngestTxResponse.ResultXDR = resultXdr + if resultXDR, exists := result["resultXdr"].(string); exists { + getIngestTxResponse.ResultXDR = resultXDR } if createdAt, exists := result["createdAt"].(string); exists { // we can supress erroneous createdAt errors as this is not an important field - createdAtInt, _ := strconv.ParseInt(createdAt, 10, 64) - getIngestTxResponse.CreatedAt = createdAtInt + createdAtInt, e := strconv.ParseInt(createdAt, 10, 64) + if e != nil { + getIngestTxResponse.Status = tss.ErrorStatus + err = fmt.Errorf("cannot parse createdAt") + } else { + getIngestTxResponse.CreatedAt = createdAtInt + } } + } else { + getIngestTxResponse.Status = tss.ErrorStatus + return getIngestTxResponse, fmt.Errorf("RPC response has no result field") + } - return getIngestTxResponse, nil + return getIngestTxResponse, err } diff --git a/internal/tss/utils/transaction_service_test.go b/internal/tss/utils/transaction_service_test.go index 174f2d6..f58d2fd 100644 --- a/internal/tss/utils/transaction_service_test.go +++ b/internal/tss/utils/transaction_service_test.go @@ -5,8 +5,10 @@ import ( "context" "encoding/json" "errors" + "fmt" "io" "net/http" + "strings" "testing" "github.com/stellar/go/clients/horizonclient" @@ -16,7 +18,7 @@ import ( "github.com/stellar/go/xdr" "github.com/stellar/wallet-backend/internal/signing" "github.com/stellar/wallet-backend/internal/tss" - tssErr "github.com/stellar/wallet-backend/internal/tss/errors" + tsserror "github.com/stellar/wallet-backend/internal/tss/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) @@ -44,38 +46,41 @@ func buildTestTransaction() *txnbuild.Transaction { } func TestValidateOptions(t *testing.T) { - t.Run("return_error_when_distribution_signature_client_null", func(t *testing.T) { + t.Run("return_error_when_distribution_signature_client_nil", func(t *testing.T) { opts := TransactionServiceOptions{ DistributionAccountSignatureClient: nil, ChannelAccountSignatureClient: &signing.SignatureClientMock{}, HorizonClient: &horizonclient.MockClient{}, - RpcUrl: "http://localhost:8000/soroban/rpc", + RPCURL: "http://localhost:8000/soroban/rpc", BaseFee: 114, + HTTPClient: &MockHTTPClient{}, } err := opts.ValidateOptions() assert.Equal(t, "distribution account signature client cannot be nil", err.Error()) }) - t.Run("return_error_when_channel_signature_client_null", func(t *testing.T) { + t.Run("return_error_when_channel_signature_client_nil", func(t *testing.T) { opts := TransactionServiceOptions{ DistributionAccountSignatureClient: &signing.SignatureClientMock{}, ChannelAccountSignatureClient: nil, HorizonClient: &horizonclient.MockClient{}, - RpcUrl: "http://localhost:8000/soroban/rpc", + RPCURL: "http://localhost:8000/soroban/rpc", BaseFee: 114, + HTTPClient: &MockHTTPClient{}, } err := opts.ValidateOptions() assert.Equal(t, "channel account signature client cannot be nil", err.Error()) }) - t.Run("return_error_when_horizon_client_null", func(t *testing.T) { + t.Run("return_error_when_horizon_client_nil", func(t *testing.T) { opts := TransactionServiceOptions{ DistributionAccountSignatureClient: &signing.SignatureClientMock{}, ChannelAccountSignatureClient: &signing.SignatureClientMock{}, HorizonClient: nil, - RpcUrl: "http://localhost:8000/soroban/rpc", + RPCURL: "http://localhost:8000/soroban/rpc", BaseFee: 114, + HTTPClient: &MockHTTPClient{}, } err := opts.ValidateOptions() assert.Equal(t, "horizon client cannot be nil", err.Error()) @@ -86,8 +91,9 @@ func TestValidateOptions(t *testing.T) { DistributionAccountSignatureClient: &signing.SignatureClientMock{}, ChannelAccountSignatureClient: &signing.SignatureClientMock{}, HorizonClient: &horizonclient.MockClient{}, - RpcUrl: "", + RPCURL: "", BaseFee: 114, + HTTPClient: &MockHTTPClient{}, } err := opts.ValidateOptions() assert.Equal(t, "rpc url cannot be empty", err.Error()) @@ -98,15 +104,28 @@ func TestValidateOptions(t *testing.T) { DistributionAccountSignatureClient: &signing.SignatureClientMock{}, ChannelAccountSignatureClient: &signing.SignatureClientMock{}, HorizonClient: &horizonclient.MockClient{}, - RpcUrl: "http://localhost:8000/soroban/rpc", + RPCURL: "http://localhost:8000/soroban/rpc", BaseFee: txnbuild.MinBaseFee - 10, + HTTPClient: &MockHTTPClient{}, } err := opts.ValidateOptions() assert.Equal(t, "base fee is lower than the minimum network fee", err.Error()) }) + + t.Run("return_error_http_client_nil", func(t *testing.T) { + opts := TransactionServiceOptions{ + DistributionAccountSignatureClient: &signing.SignatureClientMock{}, + ChannelAccountSignatureClient: &signing.SignatureClientMock{}, + HorizonClient: &horizonclient.MockClient{}, + RPCURL: "http://localhost:8000/soroban/rpc", + BaseFee: 114, + } + err := opts.ValidateOptions() + assert.Equal(t, "http client cannot be nil", err.Error()) + }) } -func TestSignAndBuildNewTransaction(t *testing.T) { +func TestSignAndBuildNewFeeBumpTransaction(t *testing.T) { distributionAccountSignatureClient := signing.SignatureClientMock{} defer distributionAccountSignatureClient.AssertExpectations(t) channelAccountSignatureClient := signing.SignatureClientMock{} @@ -117,17 +136,17 @@ func TestSignAndBuildNewTransaction(t *testing.T) { DistributionAccountSignatureClient: &distributionAccountSignatureClient, ChannelAccountSignatureClient: &channelAccountSignatureClient, HorizonClient: &horizonClient, - RpcUrl: "http://localhost:8000/soroban/rpc", + RPCURL: "http://localhost:8000/soroban/rpc", BaseFee: 114, - Ctx: context.Background(), + HTTPClient: &MockHTTPClient{}, }) txStr, _ := buildTestTransaction().Base64() t.Run("malformed_transaction_string", func(t *testing.T) { - feeBumpTx, err := txService.SignAndBuildNewTransaction("abcd") + feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), "abcd") assert.Empty(t, feeBumpTx) - assert.ErrorIs(t, tssErr.OriginalXdrMalformed, err) + assert.ErrorIs(t, tsserror.OriginalXDRMalformed, err) }) t.Run("channel_account_signature_client_get_account_public_key_err", func(t *testing.T) { @@ -136,7 +155,7 @@ func TestSignAndBuildNewTransaction(t *testing.T) { Return("", errors.New("channel accounts unavailable")). Once() - feeBumpTx, err := txService.SignAndBuildNewTransaction(txStr) + feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) assert.Empty(t, feeBumpTx) assert.Equal(t, "getting channel account public key: channel accounts unavailable", err.Error()) }) @@ -155,7 +174,7 @@ func TestSignAndBuildNewTransaction(t *testing.T) { Return(horizon.Account{}, errors.New("horizon down")). Once() - feeBumpTx, err := txService.SignAndBuildNewTransaction(txStr) + feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) assert.Empty(t, feeBumpTx) assert.Equal(t, "getting channel account details from horizon: horizon down", err.Error()) }) @@ -177,7 +196,7 @@ func TestSignAndBuildNewTransaction(t *testing.T) { Return(horizon.Account{AccountID: channelAccount.Address(), Sequence: 1}, nil). Once() - feeBumpTx, err := txService.SignAndBuildNewTransaction(txStr) + feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) assert.Empty(t, feeBumpTx) assert.Equal(t, "signing transaction with channel account: unable to sign", err.Error()) }) @@ -205,7 +224,7 @@ func TestSignAndBuildNewTransaction(t *testing.T) { Return(horizon.Account{AccountID: channelAccount.Address(), Sequence: 1}, nil). Once() - feeBumpTx, err := txService.SignAndBuildNewTransaction(txStr) + feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) assert.Empty(t, feeBumpTx) assert.Equal(t, "getting distribution account public key: client down", err.Error()) }) @@ -236,7 +255,7 @@ func TestSignAndBuildNewTransaction(t *testing.T) { Return(horizon.Account{AccountID: account.Address(), Sequence: 1}, nil). Once() - feeBumpTx, err := txService.SignAndBuildNewTransaction(txStr) + feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) assert.Empty(t, feeBumpTx) assert.Equal(t, "signing the fee bump transaction with distribution account: unable to sign", err.Error()) }) @@ -274,321 +293,346 @@ func TestSignAndBuildNewTransaction(t *testing.T) { Return(horizon.Account{AccountID: account.Address(), Sequence: 1}, nil). Once() - feeBumpTx, err := txService.SignAndBuildNewTransaction(txStr) + feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) assert.Equal(t, feeBumpTx, testFeeBumpTx) assert.Empty(t, err) }) } -type MockPost struct { - mock.Mock -} - -func (m *MockPost) Post(url string, content string, body io.Reader) (*http.Response, error) { - args := m.Called(url, content, body) - return args.Get(0).(*http.Response), args.Error(1) -} - -type MockUnMarshallRPCResponse struct { - mock.Mock -} - -func (m *MockUnMarshallRPCResponse) ReadAll(r io.Reader) ([]byte, error) { - args := m.Called(r) - return args.Get(0).(([]byte)), args.Error(1) - -} +type errorReader struct{} -type MockUnMarshalJSON struct { - mock.Mock +func (e *errorReader) Read(p []byte) (n int, err error) { + return 0, fmt.Errorf("read error") } -func (m *MockUnMarshalJSON) UnMarshalJSONBody(body []byte) (map[string]interface{}, error) { - args := m.Called(body) - if args.Get(0) == nil { - return nil, args.Error(1) - } - return args.Get(0).(map[string]interface{}), args.Error(1) +func (e *errorReader) Close() error { + return nil } -func TestCallRPC(t *testing.T) { - mockPost := MockPost{} - RpcPost = mockPost.Post - defer func() { RpcPost = http.Post }() - mockUnMarshalRPCResponse := MockUnMarshallRPCResponse{} - UnMarshalRPCResponse = mockUnMarshalRPCResponse.ReadAll - defer func() { UnMarshalRPCResponse = io.ReadAll }() - mockUnMarshalJSON := MockUnMarshalJSON{} - UnMarshalJSON = mockUnMarshalJSON.UnMarshalJSONBody - defer func() { UnMarshalJSON = parseJSONBody }() +func TestSendRPCRequest(t *testing.T) { + mockHTTPClient := MockHTTPClient{} + rpcURL := "http://localhost:8000/soroban/rpc" + txService, _ := NewTransactionService(TransactionServiceOptions{ + DistributionAccountSignatureClient: &signing.SignatureClientMock{}, + ChannelAccountSignatureClient: &signing.SignatureClientMock{}, + HorizonClient: &horizonclient.MockClient{}, + RPCURL: rpcURL, + BaseFee: 114, + HTTPClient: &mockHTTPClient, + }) + method := "sendTransaction" params := map[string]string{"transaction": "ABCD"} payload := map[string]interface{}{ "jsonrpc": "2.0", "id": 1, - "method": "sendTransaction", + "method": method, "params": params, } jsonData, _ := json.Marshal(payload) - rpcUrl := "http://localhost:8000/soroban/rpc" - t.Run("rpc_post_call_fails", func(t *testing.T) { - mockPost. - On("Post", rpcUrl, "application/json", bytes.NewBuffer(jsonData)). - Return(&http.Response{}, errors.New("connection error")). + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(&http.Response{}, errors.New("RPC Connection fail")). Once() - response, err := callRPC(rpcUrl, "sendTransaction", params) + resp, err := txService.sendRPCRequest(method, params) - assert.Empty(t, response) - assert.Equal(t, "sendTransaction: sending POST request to rpc: connection error", err.Error()) + assert.Empty(t, resp) + assert.Equal(t, "sendTransaction: sending POST request to rpc: RPC Connection fail", err.Error()) }) - t.Run("unmarshal_rpc_response_fails", func(t *testing.T) { - mockResponse := &http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(`{"mock": "response"}`)), - } - - mockPost. - On("Post", rpcUrl, "application/json", bytes.NewBuffer(jsonData)). - Return(mockResponse, nil). - Once() - mockUnMarshalRPCResponse. - On("ReadAll", mockResponse.Body). - Return([]byte{}, errors.New("bad string")). + t.Run("unmarshaling_rpc_response_fails", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(&errorReader{}), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). Once() - response, err := callRPC(rpcUrl, "sendTransaction", params) + resp, err := txService.sendRPCRequest(method, params) - assert.Empty(t, response) - assert.Equal(t, "sendTransaction: unmarshalling rpc response: bad string", err.Error()) + assert.Empty(t, resp) + assert.Equal(t, "sendTransaction: unmarshaling RPC response", err.Error()) }) - t.Run("unmarshal_json_fails", func(t *testing.T) { - mockResponse := &http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(`{"mock": "response"}`)), + t.Run("unmarshaling_json_fails", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{invalid-json`)), } - - mockPost. - On("Post", rpcUrl, "application/json", mock.AnythingOfType("*bytes.Buffer")). - Return(mockResponse, nil). + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). Once() - body := []byte("response") - mockUnMarshalRPCResponse. - On("ReadAll", mockResponse.Body). - Return(body, nil). - Once() + resp, err := txService.sendRPCRequest(method, params) - mockUnMarshalJSON. - On("UnMarshalJSONBody", body). - Return(nil, errors.New("bad json format")). - Once() - - response, err := callRPC(rpcUrl, "sendTransaction", params) - - assert.Empty(t, response) - assert.Equal(t, "sendTransaction: parsing rpc response JSON: bad json format", err.Error()) + assert.Empty(t, resp) + assert.Equal(t, "sendTransaction: parsing RPC response JSON", err.Error()) }) - t.Run("returns_unmarshalled_value", func(t *testing.T) { - mockResponse := &http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(`{"mock": "response"}`)), - } - mockPost. - On("Post", rpcUrl, "application/json", mock.AnythingOfType("*bytes.Buffer")). - Return(mockResponse, nil). - Once() - - body := []byte("response") - mockUnMarshalRPCResponse. - On("ReadAll", mockResponse.Body). - Return(body, nil). - Once() - - expectedResponse := map[string]interface{}{"jsonrpc": "2.0", "id": 1, "result": map[string]interface{}{"status": "SUCCESS", "envelopeXdr": "ABCD"}} - - mockUnMarshalJSON. - On("UnMarshalJSONBody", body). - Return(expectedResponse, nil). + t.Run("returns_rpc_response", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"status": "success"}`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). Once() - rpcResponse, err := callRPC(rpcUrl, "sendTransaction", params) + resp, err := txService.sendRPCRequest(method, params) - assert.Equal(t, rpcResponse, expectedResponse) + assert.Equal(t, resp, map[string]interface{}{"status": "success"}) assert.Empty(t, err) }) } -type MockCallRPC struct { - mock.Mock -} - -func (m *MockCallRPC) callRPC(rpcUrl string, method string, params map[string]string) (map[string]interface{}, error) { - args := m.Called(rpcUrl, method, params) - if args.Get(0) == nil { - return nil, args.Error(1) - } - return args.Get(0).(map[string]interface{}), args.Error(1) -} - -type MockUnMarshalErrorResultXdr struct { - mock.Mock -} - -func (m *MockUnMarshalErrorResultXdr) UnMarshalErrorResultXdr(errorResultXdr string) (tss.RPCTXCode, error) { - args := m.Called(errorResultXdr) - return args.Get(0).(tss.RPCTXCode), args.Error(1) -} - func TestSendTransaction(t *testing.T) { - mockCallRPC := MockCallRPC{} - callRPC = mockCallRPC.callRPC - defer func() { callRPC = sendRPCRequest }() - mockUnMarshalErrorResultXdr := MockUnMarshalErrorResultXdr{} - UnMarshalErrorResultXdr = mockUnMarshalErrorResultXdr.UnMarshalErrorResultXdr - defer func() { UnMarshalErrorResultXdr = parseErrorResultXdr }() + mockHTTPClient := MockHTTPClient{} + rpcURL := "http://localhost:8000/soroban/rpc" txService, _ := NewTransactionService(TransactionServiceOptions{ DistributionAccountSignatureClient: &signing.SignatureClientMock{}, ChannelAccountSignatureClient: &signing.SignatureClientMock{}, HorizonClient: &horizonclient.MockClient{}, - RpcUrl: "http://localhost:8000/soroban/rpc", + RPCURL: rpcURL, BaseFee: 114, + HTTPClient: &mockHTTPClient, + }) + method := "sendTransaction" + params := map[string]string{"transaction": "ABCD"} + payload := map[string]interface{}{ + "jsonrpc": "2.0", + "id": 1, + "method": method, + "params": params, + } + jsonData, _ := json.Marshal(payload) + + t.Run("rpc_request_fails", func(t *testing.T) { + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(&http.Response{}, errors.New("RPC Connection fail")). + Once() + + resp, err := txService.SendTransaction("ABCD") + + assert.Equal(t, tss.RPCFailCode, resp.Code.OtherCodes) + assert.Equal(t, "sendTransaction: sending POST request to rpc: RPC Connection fail", err.Error()) + }) - txXdr, _ := buildTestTransaction().Base64() - rpcUrl := "http://localhost:8000/soroban/rpc" - t.Run("call_rpc_returns_error", func(t *testing.T) { - mockCallRPC. - On("callRPC", rpcUrl, "sendTransaction", map[string]string{"transaction": txXdr}). - Return(nil, errors.New("unable to reach rpc server")). + t.Run("response_has_no_result_field", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"foo": "bar"}`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). Once() - rpcSendTxResponse, err := txService.SendTransaction(txXdr) - assert.Equal(t, rpcSendTxResponse.Code.OtherCodes, tss.RPCFailCode) - assert.Equal(t, "unable to reach rpc server", err.Error()) + resp, err := txService.SendTransaction("ABCD") + + assert.Equal(t, tss.RPCFailCode, resp.Code.OtherCodes) + assert.Equal(t, "RPC response has no result field", err.Error()) + }) - t.Run("error_unmarshaling_error_result_xdr", func(t *testing.T) { - errorResultXdr := "AAAAAAAAAGT////7AAAAAA==" - rpcResponse := map[string]interface{}{"jsonrpc": "2.0", "id": 1, "result": map[string]interface{}{"status": tss.ErrorStatus, "errorResultXdr": errorResultXdr}} - mockCallRPC. - On("callRPC", rpcUrl, "sendTransaction", map[string]string{"transaction": txXdr}). - Return(rpcResponse, nil). + + t.Run("response_has_status_field", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"result": {"status": "PENDING"}}`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). Once() - mockUnMarshalErrorResultXdr. - On("UnMarshalErrorResultXdr", errorResultXdr). - Return(tss.RPCTXCode{OtherCodes: tss.UnMarshalBinaryCode}, errors.New("unable to unmarshal")). + resp, err := txService.SendTransaction("ABCD") + + assert.Equal(t, tss.PendingStatus, resp.Status) + assert.Empty(t, err) + }) + + t.Run("response_has_hash_field", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"result": {"hash": "xyz"}}`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). Once() - rpcSendTxResponse, err := txService.SendTransaction(txXdr) - assert.Equal(t, rpcSendTxResponse.Status, tss.ErrorStatus) - assert.Equal(t, rpcSendTxResponse.Code.OtherCodes, tss.UnMarshalBinaryCode) - assert.Equal(t, "unable to unmarshal", err.Error()) - }) - t.Run("return_send_tx_response", func(t *testing.T) { - errorResultXdr := "AAAAAAAAAGT////7AAAAAA==" - rpcResponse := map[string]interface{}{"jsonrpc": "2.0", "id": 1, "result": map[string]interface{}{"status": tss.ErrorStatus, "errorResultXdr": errorResultXdr}} - mockCallRPC. - On("callRPC", rpcUrl, "sendTransaction", map[string]string{"transaction": txXdr}). - Return(rpcResponse, nil). + resp, err := txService.SendTransaction("ABCD") + + assert.Equal(t, "xyz", resp.TransactionHash) + assert.Empty(t, err) + }) + + t.Run("response_has_unparsable_errorResultXdr", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"result": {"errorResultXdr": "ABC123"}}`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). Once() - mockUnMarshalErrorResultXdr. - On("UnMarshalErrorResultXdr", errorResultXdr). - Return(tss.RPCTXCode{TxResultCode: xdr.TransactionResultCodeTxSuccess}, nil). + resp, err := txService.SendTransaction("ABCD") + + assert.Equal(t, tss.UnMarshalBinaryCode, resp.Code.OtherCodes) + assert.Equal(t, "unable to unmarshal errorResultXdr: ABC123", err.Error()) + }) + t.Run("response_has_errorResultXdr", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"result": {"errorResultXdr": "AAAAAAAAAMj////9AAAAAA=="}}`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). Once() - rpcSendTxResponse, err := txService.SendTransaction(txXdr) - assert.Equal(t, rpcSendTxResponse.Status, tss.ErrorStatus) - assert.Equal(t, rpcSendTxResponse.Code.TxResultCode, xdr.TransactionResultCodeTxSuccess) + resp, err := txService.SendTransaction("ABCD") + + assert.Equal(t, xdr.TransactionResultCodeTxTooLate, resp.Code.TxResultCode) assert.Empty(t, err) }) } func TestGetTransaction(t *testing.T) { - mockCallRPC := MockCallRPC{} - callRPC = mockCallRPC.callRPC - defer func() { callRPC = sendRPCRequest }() + mockHTTPClient := MockHTTPClient{} + rpcURL := "http://localhost:8000/soroban/rpc" txService, _ := NewTransactionService(TransactionServiceOptions{ DistributionAccountSignatureClient: &signing.SignatureClientMock{}, ChannelAccountSignatureClient: &signing.SignatureClientMock{}, HorizonClient: &horizonclient.MockClient{}, - RpcUrl: "http://localhost:8000/soroban/rpc", + RPCURL: rpcURL, BaseFee: 114, + HTTPClient: &mockHTTPClient, }) - txHash, _ := buildTestTransaction().HashHex("abcd") - rpcUrl := "http://localhost:8000/soroban/rpc" + method := "getTransaction" + params := map[string]string{"hash": "XYZ"} + payload := map[string]interface{}{ + "jsonrpc": "2.0", + "id": 1, + "method": method, + "params": params, + } + jsonData, _ := json.Marshal(payload) + + t.Run("rpc_request_fails", func(t *testing.T) { + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(&http.Response{}, errors.New("RPC Connection fail")). + Once() + + resp, err := txService.GetTransaction("XYZ") - t.Run("call_rpc_returns_error", func(t *testing.T) { - mockCallRPC. - On("callRPC", rpcUrl, "getTransaction", map[string]string{"hash": txHash}). - Return(nil, errors.New("unable to reach rpc server")). + assert.Equal(t, tss.ErrorStatus, resp.Status) + assert.Equal(t, "getTransaction: sending POST request to rpc: RPC Connection fail", err.Error()) + + }) + + t.Run("response_has_no_result_field", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"foo": "bar"}`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). Once() - _, err := txService.GetTransaction(txHash) - assert.Equal(t, "unable to reach rpc server", err.Error()) + resp, err := txService.GetTransaction("XYZ") + + assert.Equal(t, tss.ErrorStatus, resp.Status) + assert.Equal(t, "RPC response has no result field", err.Error()) }) - t.Run("returns_resp_with_status", func(t *testing.T) { - rpcResponse := map[string]interface{}{"jsonrpc": "2.0", "id": 1, "result": map[string]interface{}{"status": tss.SuccessStatus}} - mockCallRPC. - On("callRPC", rpcUrl, "getTransaction", map[string]string{"hash": txHash}). - Return(rpcResponse, nil). + t.Run("response_has_status_field", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"result": {"status": "SUCCESS"}}`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). Once() - getIngestTxResponse, err := txService.GetTransaction(txHash) - assert.Equal(t, getIngestTxResponse.Status, tss.SuccessStatus) - assert.Empty(t, getIngestTxResponse.EnvelopeXDR) - assert.Empty(t, getIngestTxResponse.ResultXDR) - assert.Empty(t, getIngestTxResponse.CreatedAt) + resp, err := txService.GetTransaction("XYZ") + + assert.Equal(t, tss.SuccessStatus, resp.Status) assert.Empty(t, err) }) - t.Run("returns_resp_with_envelope_xdr", func(t *testing.T) { - rpcResponse := map[string]interface{}{"jsonrpc": "2.0", "id": 1, "result": map[string]interface{}{"envelopeXdr": "abcd"}} - mockCallRPC. - On("callRPC", rpcUrl, "getTransaction", map[string]string{"hash": txHash}). - Return(rpcResponse, nil). + t.Run("response_has_envelopeXdr_field", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"result": {"envelopeXdr": "envelopeABCD"}}`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). Once() - getIngestTxResponse, err := txService.GetTransaction(txHash) - assert.Empty(t, getIngestTxResponse.Status) - assert.Equal(t, getIngestTxResponse.EnvelopeXDR, "abcd") - assert.Empty(t, getIngestTxResponse.ResultXDR) - assert.Empty(t, getIngestTxResponse.CreatedAt) + resp, err := txService.GetTransaction("XYZ") + + assert.Equal(t, "envelopeABCD", resp.EnvelopeXDR) assert.Empty(t, err) }) - t.Run("returns_resp_with_result_xdr", func(t *testing.T) { - rpcResponse := map[string]interface{}{"jsonrpc": "2.0", "id": 1, "result": map[string]interface{}{"resultXdr": "abcd"}} - mockCallRPC. - On("callRPC", rpcUrl, "getTransaction", map[string]string{"hash": txHash}). - Return(rpcResponse, nil). + t.Run("response_has_resultXdr_field", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"result": {"resultXdr": "resultABCD"}}`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). Once() - getIngestTxResponse, err := txService.GetTransaction(txHash) - assert.Empty(t, getIngestTxResponse.Status) - assert.Empty(t, getIngestTxResponse.EnvelopeXDR) - assert.Equal(t, getIngestTxResponse.ResultXDR, "abcd") - assert.Empty(t, getIngestTxResponse.CreatedAt) + resp, err := txService.GetTransaction("XYZ") + + assert.Equal(t, "resultABCD", resp.ResultXDR) assert.Empty(t, err) }) - t.Run("returns_resp_with_created_at", func(t *testing.T) { - rpcResponse := map[string]interface{}{"jsonrpc": "2.0", "id": 1, "result": map[string]interface{}{"createdAt": "1234"}} - mockCallRPC. - On("callRPC", rpcUrl, "getTransaction", map[string]string{"hash": txHash}). - Return(rpcResponse, nil). + t.Run("unable_to_parse_createdAt", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"result": {"createdAt": "ABCD"}}`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). Once() - getIngestTxResponse, err := txService.GetTransaction(txHash) - assert.Empty(t, getIngestTxResponse.Status) - assert.Empty(t, getIngestTxResponse.EnvelopeXDR) - assert.Empty(t, getIngestTxResponse.ResultXDR) - assert.Equal(t, getIngestTxResponse.CreatedAt, int64(1234)) + resp, err := txService.GetTransaction("XYZ") + + assert.Equal(t, tss.ErrorStatus, resp.Status) + assert.Equal(t, "cannot parse createdAt", err.Error()) + }) + + t.Run("response_has_createdAt_field", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"result": {"createdAt": "1234567"}}`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). + Once() + + resp, err := txService.GetTransaction("XYZ") + + assert.Equal(t, int64(1234567), resp.CreatedAt) assert.Empty(t, err) }) + } diff --git a/internal/tss/utils/types.go b/internal/tss/utils/types.go deleted file mode 100644 index 1cec932..0000000 --- a/internal/tss/utils/types.go +++ /dev/null @@ -1,13 +0,0 @@ -package utils - -import ( - "github.com/stellar/go/txnbuild" - "github.com/stellar/wallet-backend/internal/tss" -) - -type TransactionService interface { - NetworkPassPhrase() string - SignAndBuildNewTransaction(origTxXdr string) (*txnbuild.FeeBumpTransaction, error) - SendTransaction(transactionXdr string) (tss.RPCSendTxResponse, error) - GetTransaction(transactionHash string) (tss.RPCGetIngestTxResponse, error) -} From 968a6b71bd3d1fc8fa727aeb626c8199dba108eb Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Wed, 18 Sep 2024 00:37:00 -0700 Subject: [PATCH 034/113] lint deadcode error - suppress for now --- internal/tss/utils/transaction_service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/tss/utils/transaction_service.go b/internal/tss/utils/transaction_service.go index 720f8bd..d24a079 100644 --- a/internal/tss/utils/transaction_service.go +++ b/internal/tss/utils/transaction_service.go @@ -125,6 +125,7 @@ func (t *transactionService) sendRPCRequest(method string, params map[string]str return res, nil } +// nolint:deadcode func (t *transactionService) NetworkPassphrase() string { return t.DistributionAccountSignatureClient.NetworkPassphrase() } @@ -254,7 +255,6 @@ func (t *transactionService) GetTransaction(transactionHash string) (tss.RPCGetI getIngestTxResponse.ResultXDR = resultXDR } if createdAt, exists := result["createdAt"].(string); exists { - // we can supress erroneous createdAt errors as this is not an important field createdAtInt, e := strconv.ParseInt(createdAt, 10, 64) if e != nil { getIngestTxResponse.Status = tss.ErrorStatus From 096c7bd53cc9531e2e0ec0dcc0e953632cfda62e Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Wed, 18 Sep 2024 00:47:48 -0700 Subject: [PATCH 035/113] removed deadcode --- internal/tss/utils/transaction_service.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/internal/tss/utils/transaction_service.go b/internal/tss/utils/transaction_service.go index d24a079..164d0f4 100644 --- a/internal/tss/utils/transaction_service.go +++ b/internal/tss/utils/transaction_service.go @@ -25,7 +25,6 @@ type HTTPClient interface { } type TransactionService interface { - NetworkPassphrase() string SignAndBuildNewFeeBumpTransaction(ctx context.Context, origTxXdr string) (*txnbuild.FeeBumpTransaction, error) SendTransaction(transactionXdr string) (tss.RPCSendTxResponse, error) GetTransaction(transactionHash string) (tss.RPCGetIngestTxResponse, error) @@ -125,11 +124,6 @@ func (t *transactionService) sendRPCRequest(method string, params map[string]str return res, nil } -// nolint:deadcode -func (t *transactionService) NetworkPassphrase() string { - return t.DistributionAccountSignatureClient.NetworkPassphrase() -} - func (t *transactionService) SignAndBuildNewFeeBumpTransaction(ctx context.Context, origTxXdr string) (*txnbuild.FeeBumpTransaction, error) { genericTx, err := txnbuild.TransactionFromXDR(origTxXdr) if err != nil { From 6cfc3fd66d8cf6b474de4dcd3dfc70a80ce96c71 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Wed, 18 Sep 2024 01:59:24 -0700 Subject: [PATCH 036/113] changes after comments on transaction service pr --- internal/serve/serve.go | 6 +- .../channels/rpc_caller_service_channel.go | 18 +- .../rpc_caller_service_channel_test.go | 20 +- internal/tss/channels/types.go | 6 - internal/tss/errors/errors.go | 2 +- internal/tss/services/rpc_caller_service.go | 2 + internal/tss/store/store.go | 23 +- internal/tss/store/store_test.go | 20 +- internal/tss/store/types.go | 10 - internal/tss/utils/mocks.go | 21 +- internal/tss/utils/transaction_service.go | 169 +++--- .../tss/utils/transaction_service_test.go | 554 ++++++++++-------- internal/tss/utils/types.go | 13 - 13 files changed, 478 insertions(+), 386 deletions(-) delete mode 100644 internal/tss/channels/types.go delete mode 100644 internal/tss/store/types.go delete mode 100644 internal/tss/utils/types.go diff --git a/internal/serve/serve.go b/internal/serve/serve.go index 37c10ee..7261eb5 100644 --- a/internal/serve/serve.go +++ b/internal/serve/serve.go @@ -168,14 +168,12 @@ func initHandlerDeps(cfg Configs) (handlerDeps, error) { go ensureChannelAccounts(channelAccountService, int64(cfg.NumberOfChannelAccounts)) // TSS - ctx := context.Background() txServiceOpts := tssutils.TransactionServiceOptions{ DistributionAccountSignatureClient: cfg.DistributionAccountSignatureClient, ChannelAccountSignatureClient: cfg.ChannelAccountSignatureClient, HorizonClient: &horizonClient, - RpcUrl: cfg.RpcUrl, + RPCURL: cfg.RpcUrl, BaseFee: int64(cfg.BaseFee), // Reuse horizon base fee for RPC?? - Ctx: ctx, } tssTxService, err := tssutils.NewTransactionService(txServiceOpts) if err != nil { @@ -183,7 +181,7 @@ func initHandlerDeps(cfg Configs) (handlerDeps, error) { } // re-use same context as above?? - store := tssstore.NewStore(ctx, dbConnectionPool) + store := tssstore.NewStore(dbConnectionPool) errorHandlerService := tssservices.NewErrorHandlerService(nil) tssChannelConfigs := tsschannel.RPCCallerServiceChannelConfigs{ Store: store, diff --git a/internal/tss/channels/rpc_caller_service_channel.go b/internal/tss/channels/rpc_caller_service_channel.go index 233b4d1..e75b899 100644 --- a/internal/tss/channels/rpc_caller_service_channel.go +++ b/internal/tss/channels/rpc_caller_service_channel.go @@ -1,6 +1,8 @@ package channels import ( + "context" + "github.com/alitto/pond" "github.com/stellar/go/support/log" "github.com/stellar/wallet-backend/internal/tss" @@ -18,7 +20,7 @@ type RPCCallerServiceChannelConfigs struct { } type rpcCallerServicePool struct { - pool WorkerPool + pool *pond.WorkerPool txService utils.TransactionService errHandlerService tss_services.Service store tss_store.Store @@ -42,7 +44,9 @@ func (p *rpcCallerServicePool) Send(payload tss.Payload) { func (p *rpcCallerServicePool) Receive(payload tss.Payload) { - err := p.store.UpsertTransaction(payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.NewStatus) + ctx := context.Background() + + err := p.store.UpsertTransaction(ctx, payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.NewStatus) if err != nil { log.Errorf("RPCCallerService: Unable to upsert transaction into transactions table: %s", err.Error()) return @@ -52,12 +56,12 @@ func (p *rpcCallerServicePool) Receive(payload tss.Payload) { stays at NEW, so that it does not progress any further and can be picked up for re-processing when this pool is restarted. */ - feeBumpTx, err := p.txService.SignAndBuildNewTransaction(payload.TransactionXDR) + feeBumpTx, err := p.txService.SignAndBuildNewFeeBumpTransaction(ctx, payload.TransactionXDR) if err != nil { log.Errorf("RPCCallerService: Unable to sign/build transaction: %s", err.Error()) return } - feeBumpTxHash, err := feeBumpTx.HashHex(p.txService.NetworkPassPhrase()) + feeBumpTxHash, err := feeBumpTx.HashHex(p.txService.NetworkPassphrase()) if err != nil { log.Errorf("RPCCallerService: Unable to hashhex fee bump transaction: %s", err.Error()) return @@ -68,14 +72,14 @@ func (p *rpcCallerServicePool) Receive(payload tss.Payload) { log.Errorf("RPCCallerService: Unable to base64 fee bump transaction: %s", err.Error()) return } - err = p.store.UpsertTry(payload.TransactionHash, feeBumpTxHash, feeBumpTxXDR, tss.RPCTXCode{OtherCodes: tss.NewCode}) + err = p.store.UpsertTry(ctx, payload.TransactionHash, feeBumpTxHash, feeBumpTxXDR, tss.RPCTXCode{OtherCodes: tss.NewCode}) if err != nil { log.Errorf("RPCCallerService: Unable to upsert try in tries table: %s", err.Error()) return } rpcSendResp, rpcErr := p.txService.SendTransaction(feeBumpTxXDR) - err = p.store.UpsertTry(payload.TransactionHash, feeBumpTxHash, feeBumpTxXDR, rpcSendResp.Code) + err = p.store.UpsertTry(ctx, payload.TransactionHash, feeBumpTxHash, feeBumpTxXDR, rpcSendResp.Code) if err != nil { log.Errorf("RPCCallerService: Unable to upsert try in tries table: %s", err.Error()) return @@ -86,7 +90,7 @@ func (p *rpcCallerServicePool) Receive(payload tss.Payload) { return } - err = p.store.UpsertTransaction(payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, rpcSendResp.Status) + err = p.store.UpsertTransaction(ctx, payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, rpcSendResp.Status) if err != nil { log.Errorf("RPCCallerService:Unable to do the final update of tx in the transactions table: %s", err.Error()) return diff --git a/internal/tss/channels/rpc_caller_service_channel_test.go b/internal/tss/channels/rpc_caller_service_channel_test.go index 3a19742..636412e 100644 --- a/internal/tss/channels/rpc_caller_service_channel_test.go +++ b/internal/tss/channels/rpc_caller_service_channel_test.go @@ -23,7 +23,7 @@ func TestSend(t *testing.T) { dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) require.NoError(t, err) defer dbConnectionPool.Close() - store := store.NewStore(context.Background(), dbConnectionPool) + store := store.NewStore(dbConnectionPool) txServiceMock := utils.TransactionServiceMock{} cfgs := RPCCallerServiceChannelConfigs{ Store: store, @@ -37,7 +37,7 @@ func TestSend(t *testing.T) { payload.TransactionHash = "hash" payload.TransactionXDR = "xdr" txServiceMock. - On("SignAndBuildNewTransaction", payload.TransactionXDR). + On("SignAndBuildNewFeeBumpTransaction", payload.TransactionXDR). Return(nil, errors.New("signing failed")) channel.Send(payload) channel.Stop() @@ -55,7 +55,7 @@ func TestReceive(t *testing.T) { dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) require.NoError(t, err) defer dbConnectionPool.Close() - store := store.NewStore(context.Background(), dbConnectionPool) + store := store.NewStore(dbConnectionPool) txServiceMock := utils.TransactionServiceMock{} errHandlerService := tss_services.MockService{} cfgs := RPCCallerServiceChannelConfigs{ @@ -75,7 +75,7 @@ func TestReceive(t *testing.T) { t.Run("fail_on_tx_build_and_sign", func(t *testing.T) { txServiceMock. - On("SignAndBuildNewTransaction", payload.TransactionXDR). + On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). Return(nil, errors.New("signing failed")). Once() channel.Receive(payload) @@ -91,10 +91,10 @@ func TestReceive(t *testing.T) { sendResp := tss.RPCSendTxResponse{} sendResp.Code.OtherCodes = tss.RPCFailCode txServiceMock. - On("SignAndBuildNewTransaction", payload.TransactionXDR). + On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). Return(feeBumpTx, nil). Once(). - On("NetworkPassPhrase"). + On("NetworkPassphrase"). Return(networkPass). Once(). On("SendTransaction", txXDR). @@ -121,10 +121,10 @@ func TestReceive(t *testing.T) { sendResp := tss.RPCSendTxResponse{} sendResp.Code.OtherCodes = tss.UnMarshalBinaryCode txServiceMock. - On("SignAndBuildNewTransaction", payload.TransactionXDR). + On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). Return(feeBumpTx, nil). Once(). - On("NetworkPassPhrase"). + On("NetworkPassphrase"). Return(networkPass). Once(). On("SendTransaction", txXDR). @@ -154,10 +154,10 @@ func TestReceive(t *testing.T) { sendResp.TransactionXDR = feeBumpTxXDR sendResp.Code.TxResultCode = xdr.TransactionResultCodeTxTooEarly txServiceMock. - On("SignAndBuildNewTransaction", payload.TransactionXDR). + On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). Return(feeBumpTx, nil). Once(). - On("NetworkPassPhrase"). + On("NetworkPassphrase"). Return(networkPass). Once(). On("SendTransaction", feeBumpTxXDR). diff --git a/internal/tss/channels/types.go b/internal/tss/channels/types.go deleted file mode 100644 index 65ab5e9..0000000 --- a/internal/tss/channels/types.go +++ /dev/null @@ -1,6 +0,0 @@ -package channels - -type WorkerPool interface { - Submit(task func()) - StopAndWait() -} diff --git a/internal/tss/errors/errors.go b/internal/tss/errors/errors.go index 8cb3bc9..69f8bc3 100644 --- a/internal/tss/errors/errors.go +++ b/internal/tss/errors/errors.go @@ -5,5 +5,5 @@ import ( ) var ( - OriginalXdrMalformed = errors.New("transaction string is malformed") + OriginalXDRMalformed = errors.New("transaction string is malformed") ) diff --git a/internal/tss/services/rpc_caller_service.go b/internal/tss/services/rpc_caller_service.go index 5cbaaba..7f539eb 100644 --- a/internal/tss/services/rpc_caller_service.go +++ b/internal/tss/services/rpc_caller_service.go @@ -8,6 +8,8 @@ type rpcCallerService struct { channel tss.Channel } +var _ Service = (*rpcCallerService)(nil) + func NewRPCCallerService(channel tss.Channel) Service { return &rpcCallerService{ channel: channel, diff --git a/internal/tss/store/store.go b/internal/tss/store/store.go index 7daac66..5dcc97f 100644 --- a/internal/tss/store/store.go +++ b/internal/tss/store/store.go @@ -8,19 +8,24 @@ import ( "github.com/stellar/wallet-backend/internal/tss" ) +type Store interface { + UpsertTransaction(ctx context.Context, WebhookURL string, txHash string, txXDR string, status tss.RPCTXStatus) error + UpsertTry(ctx context.Context, transactionHash string, feeBumpTxHash string, feeBumpTxXDR string, status tss.RPCTXCode) error +} + +var _ Store = (*store)(nil) + type store struct { - DB db.ConnectionPool - ctx context.Context + DB db.ConnectionPool } -func NewStore(ctx context.Context, db db.ConnectionPool) Store { +func NewStore(db db.ConnectionPool) Store { return &store{ - DB: db, - ctx: ctx, + DB: db, } } -func (s *store) UpsertTransaction(webhookURL string, txHash string, txXDR string, status tss.RPCTXStatus) error { +func (s *store) UpsertTransaction(ctx context.Context, webhookURL string, txHash string, txXDR string, status tss.RPCTXStatus) error { const q = ` INSERT INTO tss_transactions (transaction_hash, transaction_xdr, webhook_url, current_status) @@ -33,14 +38,14 @@ func (s *store) UpsertTransaction(webhookURL string, txHash string, txXDR string current_status = $4, updated_at = NOW(); ` - _, err := s.DB.ExecContext(s.ctx, q, txHash, txXDR, webhookURL, string(status)) + _, err := s.DB.ExecContext(ctx, q, txHash, txXDR, webhookURL, string(status)) if err != nil { return fmt.Errorf("inserting/updatig tss transaction: %w", err) } return nil } -func (s *store) UpsertTry(txHash string, feeBumpTxHash string, feeBumpTxXDR string, status tss.RPCTXCode) error { +func (s *store) UpsertTry(ctx context.Context, txHash string, feeBumpTxHash string, feeBumpTxXDR string, status tss.RPCTXCode) error { const q = ` INSERT INTO tss_transaction_submission_tries (original_transaction_hash, try_transaction_hash, try_transaction_xdr, status) @@ -60,7 +65,7 @@ func (s *store) UpsertTry(txHash string, feeBumpTxHash string, feeBumpTxXDR stri } else { st = int(status.TxResultCode) } - _, err := s.DB.ExecContext(s.ctx, q, txHash, feeBumpTxHash, feeBumpTxXDR, st) + _, err := s.DB.ExecContext(ctx, q, txHash, feeBumpTxHash, feeBumpTxXDR, st) if err != nil { return fmt.Errorf("inserting/updating tss try: %w", err) } diff --git a/internal/tss/store/store_test.go b/internal/tss/store/store_test.go index c813f5b..57709ab 100644 --- a/internal/tss/store/store_test.go +++ b/internal/tss/store/store_test.go @@ -18,9 +18,9 @@ func TestUpsertTransaction(t *testing.T) { dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) require.NoError(t, err) defer dbConnectionPool.Close() - store := NewStore(context.Background(), dbConnectionPool) + store := NewStore(dbConnectionPool) t.Run("insert", func(t *testing.T) { - _ = store.UpsertTransaction("www.stellar.org", "hash", "xdr", tss.NewStatus) + _ = store.UpsertTransaction(context.Background(), "www.stellar.org", "hash", "xdr", tss.NewStatus) var status string err = dbConnectionPool.GetContext(context.Background(), &status, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, "hash") @@ -29,8 +29,8 @@ func TestUpsertTransaction(t *testing.T) { }) t.Run("update", func(t *testing.T) { - _ = store.UpsertTransaction("www.stellar.org", "hash", "xdr", tss.NewStatus) - _ = store.UpsertTransaction("www.stellar.org", "hash", "xdr", tss.SuccessStatus) + _ = store.UpsertTransaction(context.Background(), "www.stellar.org", "hash", "xdr", tss.NewStatus) + _ = store.UpsertTransaction(context.Background(), "www.stellar.org", "hash", "xdr", tss.SuccessStatus) var status string err = dbConnectionPool.GetContext(context.Background(), &status, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, "hash") @@ -51,10 +51,10 @@ func TestUpsertTry(t *testing.T) { dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) require.NoError(t, err) defer dbConnectionPool.Close() - store := NewStore(context.Background(), dbConnectionPool) + store := NewStore(dbConnectionPool) t.Run("insert", func(t *testing.T) { code := tss.RPCTXCode{OtherCodes: tss.NewCode} - _ = store.UpsertTry("hash", "feebumptxhash", "feebumptxxdr", code) + _ = store.UpsertTry(context.Background(), "hash", "feebumptxhash", "feebumptxxdr", code) var status int err = dbConnectionPool.GetContext(context.Background(), &status, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, "feebumptxhash") @@ -64,9 +64,9 @@ func TestUpsertTry(t *testing.T) { t.Run("update_other_code", func(t *testing.T) { code := tss.RPCTXCode{OtherCodes: tss.NewCode} - _ = store.UpsertTry("hash", "feebumptxhash", "feebumptxxdr", code) + _ = store.UpsertTry(context.Background(), "hash", "feebumptxhash", "feebumptxxdr", code) code = tss.RPCTXCode{OtherCodes: tss.RPCFailCode} - _ = store.UpsertTry("hash", "feebumptxhash", "feebumptxxdr", code) + _ = store.UpsertTry(context.Background(), "hash", "feebumptxhash", "feebumptxxdr", code) var status int err = dbConnectionPool.GetContext(context.Background(), &status, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, "feebumptxhash") require.NoError(t, err) @@ -80,9 +80,9 @@ func TestUpsertTry(t *testing.T) { t.Run("update_tx_code", func(t *testing.T) { code := tss.RPCTXCode{OtherCodes: tss.NewCode} - _ = store.UpsertTry("hash", "feebumptxhash", "feebumptxxdr", code) + _ = store.UpsertTry(context.Background(), "hash", "feebumptxhash", "feebumptxxdr", code) code = tss.RPCTXCode{TxResultCode: xdr.TransactionResultCodeTxSuccess} - _ = store.UpsertTry("hash", "feebumptxhash", "feebumptxxdr", code) + _ = store.UpsertTry(context.Background(), "hash", "feebumptxhash", "feebumptxxdr", code) var status int err = dbConnectionPool.GetContext(context.Background(), &status, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, "feebumptxhash") require.NoError(t, err) diff --git a/internal/tss/store/types.go b/internal/tss/store/types.go deleted file mode 100644 index 645e081..0000000 --- a/internal/tss/store/types.go +++ /dev/null @@ -1,10 +0,0 @@ -package store - -import ( - "github.com/stellar/wallet-backend/internal/tss" -) - -type Store interface { - UpsertTransaction(WebhookURL string, txHash string, txXDR string, status tss.RPCTXStatus) error - UpsertTry(transactionHash string, feeBumpTxHash string, feeBumpTxXDR string, status tss.RPCTXCode) error -} diff --git a/internal/tss/utils/mocks.go b/internal/tss/utils/mocks.go index 2a95cdf..f090df6 100644 --- a/internal/tss/utils/mocks.go +++ b/internal/tss/utils/mocks.go @@ -1,24 +1,39 @@ package utils import ( + "context" + "fmt" + "io" + "net/http" + "github.com/stellar/go/txnbuild" "github.com/stellar/wallet-backend/internal/tss" "github.com/stretchr/testify/mock" ) +type MockHTTPClient struct { + mock.Mock +} + +func (s *MockHTTPClient) Post(url, contentType string, body io.Reader) (resp *http.Response, err error) { + args := s.Called(url, contentType, body) + return args.Get(0).(*http.Response), args.Error(1) +} + type TransactionServiceMock struct { mock.Mock } var _ TransactionService = (*TransactionServiceMock)(nil) -func (t *TransactionServiceMock) NetworkPassPhrase() string { +func (t *TransactionServiceMock) NetworkPassphrase() string { args := t.Called() return args.String(0) } -func (t *TransactionServiceMock) SignAndBuildNewTransaction(origTxXdr string) (*txnbuild.FeeBumpTransaction, error) { - args := t.Called(origTxXdr) +func (t *TransactionServiceMock) SignAndBuildNewFeeBumpTransaction(ctx context.Context, origTxXdr string) (*txnbuild.FeeBumpTransaction, error) { + fmt.Println("INSIDE SignAndBuildNewFeeBumpTransaction mock") + args := t.Called(ctx, origTxXdr) if result := args.Get(0); result != nil { return result.(*txnbuild.FeeBumpTransaction), args.Error(1) } diff --git a/internal/tss/utils/transaction_service.go b/internal/tss/utils/transaction_service.go index e38b885..35f89a0 100644 --- a/internal/tss/utils/transaction_service.go +++ b/internal/tss/utils/transaction_service.go @@ -3,44 +3,53 @@ package utils import ( "bytes" "context" + "encoding/base64" "encoding/json" "fmt" "io" "net/http" "strconv" + "strings" + xdr3 "github.com/stellar/go-xdr/xdr3" "github.com/stellar/go/clients/horizonclient" "github.com/stellar/go/txnbuild" "github.com/stellar/go/xdr" "github.com/stellar/wallet-backend/internal/signing" "github.com/stellar/wallet-backend/internal/tss" - tssErr "github.com/stellar/wallet-backend/internal/tss/errors" + tsserror "github.com/stellar/wallet-backend/internal/tss/errors" ) -var ( - RpcPost = http.Post - UnMarshalRPCResponse = io.ReadAll - UnMarshalJSON = parseJSONBody - callRPC = sendRPCRequest - UnMarshalErrorResultXdr = parseErrorResultXdr -) +type HTTPClient interface { + Post(url string, t string, body io.Reader) (resp *http.Response, err error) +} + +type TransactionService interface { + NetworkPassphrase() string + SignAndBuildNewFeeBumpTransaction(ctx context.Context, origTxXdr string) (*txnbuild.FeeBumpTransaction, error) + SendTransaction(transactionXdr string) (tss.RPCSendTxResponse, error) + GetTransaction(transactionHash string) (tss.RPCGetIngestTxResponse, error) +} type transactionService struct { DistributionAccountSignatureClient signing.SignatureClient ChannelAccountSignatureClient signing.SignatureClient HorizonClient horizonclient.ClientInterface - RpcUrl string + RPCURL string BaseFee int64 + HTTPClient HTTPClient Ctx context.Context } +var _ TransactionService = (*transactionService)(nil) + type TransactionServiceOptions struct { DistributionAccountSignatureClient signing.SignatureClient ChannelAccountSignatureClient signing.SignatureClient HorizonClient horizonclient.ClientInterface - RpcUrl string + RPCURL string BaseFee int64 - Ctx context.Context + HTTPClient HTTPClient } func (o *TransactionServiceOptions) ValidateOptions() error { @@ -56,17 +65,22 @@ func (o *TransactionServiceOptions) ValidateOptions() error { return fmt.Errorf("horizon client cannot be nil") } - if o.RpcUrl == "" { + if o.RPCURL == "" { return fmt.Errorf("rpc url cannot be empty") } if o.BaseFee < int64(txnbuild.MinBaseFee) { return fmt.Errorf("base fee is lower than the minimum network fee") } + + if o.HTTPClient == nil { + return fmt.Errorf("http client cannot be nil") + } + return nil } -func NewTransactionService(opts TransactionServiceOptions) (TransactionService, error) { +func NewTransactionService(opts TransactionServiceOptions) (*transactionService, error) { if err := opts.ValidateOptions(); err != nil { return nil, err } @@ -74,73 +88,57 @@ func NewTransactionService(opts TransactionServiceOptions) (TransactionService, DistributionAccountSignatureClient: opts.DistributionAccountSignatureClient, ChannelAccountSignatureClient: opts.ChannelAccountSignatureClient, HorizonClient: opts.HorizonClient, - RpcUrl: opts.RpcUrl, + RPCURL: opts.RPCURL, BaseFee: opts.BaseFee, - Ctx: opts.Ctx, + HTTPClient: opts.HTTPClient, }, nil } -func parseJSONBody(body []byte) (map[string]interface{}, error) { - var res map[string]interface{} - err := json.Unmarshal(body, &res) - if err != nil { - return nil, fmt.Errorf(err.Error()) - } - return res, nil -} - -func parseErrorResultXdr(errorResultXdr string) (tss.RPCTXCode, error) { - errorResult := xdr.TransactionResult{} - err := errorResult.UnmarshalBinary([]byte(errorResultXdr)) - - if err != nil { - return tss.RPCTXCode{OtherCodes: tss.UnMarshalBinaryCode}, fmt.Errorf("SendTransaction: unable to unmarshal errorResultXdr: %s", errorResultXdr) - } - return tss.RPCTXCode{ - TxResultCode: errorResult.Result.Code, - }, nil -} - -func sendRPCRequest(rpcUrl string, method string, params map[string]string) (map[string]interface{}, error) { +func (t *transactionService) sendRPCRequest(method string, params map[string]string) (map[string]interface{}, error) { payload := map[string]interface{}{ "jsonrpc": "2.0", "id": 1, "method": method, "params": params, } - jsonData, _ := json.Marshal(payload) + jsonData, err := json.Marshal(payload) + + if err != nil { + return nil, fmt.Errorf("marshaling payload") + } - resp, err := RpcPost(rpcUrl, "application/json", bytes.NewBuffer(jsonData)) + resp, err := t.HTTPClient.Post(t.RPCURL, "application/json", bytes.NewBuffer(jsonData)) if err != nil { - return nil, fmt.Errorf(method+": sending POST request to rpc: %v", err) + return nil, fmt.Errorf("%s: sending POST request to rpc: %v", method, err) } defer resp.Body.Close() - body, err := UnMarshalRPCResponse(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf(method+": unmarshalling rpc response: %v", err) + return nil, fmt.Errorf("%s: unmarshaling RPC response", method) } - res, err := UnMarshalJSON(body) + var res map[string]interface{} + err = json.Unmarshal(body, &res) if err != nil { - return nil, fmt.Errorf(method+": parsing rpc response JSON: %v", err) + return nil, fmt.Errorf("%s: parsing RPC response JSON", method) } return res, nil } -func (t *transactionService) NetworkPassPhrase() string { +func (t *transactionService) NetworkPassphrase() string { return t.DistributionAccountSignatureClient.NetworkPassphrase() } -func (t *transactionService) SignAndBuildNewTransaction(origTxXdr string) (*txnbuild.FeeBumpTransaction, error) { +func (t *transactionService) SignAndBuildNewFeeBumpTransaction(ctx context.Context, origTxXdr string) (*txnbuild.FeeBumpTransaction, error) { genericTx, err := txnbuild.TransactionFromXDR(origTxXdr) if err != nil { - return nil, tssErr.OriginalXdrMalformed + return nil, tsserror.OriginalXDRMalformed } originalTx, txEmpty := genericTx.Transaction() if !txEmpty { - return nil, tssErr.OriginalXdrMalformed + return nil, tsserror.OriginalXDRMalformed } - channelAccountPublicKey, err := t.ChannelAccountSignatureClient.GetAccountPublicKey(t.Ctx) + channelAccountPublicKey, err := t.ChannelAccountSignatureClient.GetAccountPublicKey(ctx) if err != nil { return nil, fmt.Errorf("getting channel account public key: %w", err) } @@ -162,12 +160,12 @@ func (t *transactionService) SignAndBuildNewTransaction(origTxXdr string) (*txnb if err != nil { return nil, fmt.Errorf("building transaction: %w", err) } - tx, err = t.ChannelAccountSignatureClient.SignStellarTransaction(t.Ctx, tx, channelAccountPublicKey) + tx, err = t.ChannelAccountSignatureClient.SignStellarTransaction(ctx, tx, channelAccountPublicKey) if err != nil { return nil, fmt.Errorf("signing transaction with channel account: %w", err) } - // wrap the transaction in a fee bump tx, signed by the distribution account - distributionAccountPublicKey, err := t.DistributionAccountSignatureClient.GetAccountPublicKey(t.Ctx) + // Wrap the transaction in a fee bump tx, signed by the distribution account + distributionAccountPublicKey, err := t.DistributionAccountSignatureClient.GetAccountPublicKey(ctx) if err != nil { return nil, fmt.Errorf("getting distribution account public key: %w", err) } @@ -183,15 +181,37 @@ func (t *transactionService) SignAndBuildNewTransaction(origTxXdr string) (*txnb return nil, fmt.Errorf("building fee-bump transaction %w", err) } - feeBumpTx, err = t.DistributionAccountSignatureClient.SignStellarFeeBumpTransaction(t.Ctx, feeBumpTx) + feeBumpTx, err = t.DistributionAccountSignatureClient.SignStellarFeeBumpTransaction(ctx, feeBumpTx) if err != nil { return nil, fmt.Errorf("signing the fee bump transaction with distribution account: %w", err) } return feeBumpTx, nil } +func (t *transactionService) parseErrorResultXDR(errorResultXdr string) (tss.RPCTXCode, error) { + + //errorResult := xdr.TransactionResult{} + unMarshallErr := "unable to unmarshal errorResultXdr: %s" + //err := errorResult.UnmarshalBinary([]byte(errorResultXdr)) + + decodedBytes, err := base64.StdEncoding.DecodeString(errorResultXdr) + if err != nil { + return tss.RPCTXCode{OtherCodes: tss.UnMarshalBinaryCode}, fmt.Errorf(unMarshallErr, errorResultXdr) + } + dec := xdr3.NewDecoder(strings.NewReader(string(decodedBytes))) + var errorResult xdr.TransactionResult + _, err = dec.Decode(&errorResult) + + if err != nil { + return tss.RPCTXCode{OtherCodes: tss.UnMarshalBinaryCode}, fmt.Errorf(unMarshallErr, errorResultXdr) + } + return tss.RPCTXCode{ + TxResultCode: errorResult.Result.Code, + }, nil +} + func (t *transactionService) SendTransaction(transactionXdr string) (tss.RPCSendTxResponse, error) { - rpcResponse, err := callRPC(t.RpcUrl, "sendTransaction", map[string]string{"transaction": transactionXdr}) + rpcResponse, err := t.sendRPCRequest("sendTransaction", map[string]string{"transaction": transactionXdr}) sendTxResponse := tss.RPCSendTxResponse{} sendTxResponse.TransactionXDR = transactionXdr if err != nil { @@ -200,41 +220,52 @@ func (t *transactionService) SendTransaction(transactionXdr string) (tss.RPCSend } if result, ok := rpcResponse["result"].(map[string]interface{}); ok { - if val, exists := result["status"].(tss.RPCTXStatus); exists { - sendTxResponse.Status = val + if val, exists := result["status"].(string); exists { + sendTxResponse.Status = tss.RPCTXStatus(val) } if val, exists := result["errorResultXdr"].(string); exists { - sendTxResponse.Code, err = UnMarshalErrorResultXdr(val) + sendTxResponse.Code, err = t.parseErrorResultXDR(val) } if hash, exists := result["hash"].(string); exists { sendTxResponse.TransactionHash = hash } + } else { + sendTxResponse.Code.OtherCodes = tss.RPCFailCode + return sendTxResponse, fmt.Errorf("RPC response has no result field") } return sendTxResponse, err } func (t *transactionService) GetTransaction(transactionHash string) (tss.RPCGetIngestTxResponse, error) { - rpcResponse, err := callRPC(t.RpcUrl, "getTransaction", map[string]string{"hash": transactionHash}) + rpcResponse, err := t.sendRPCRequest("getTransaction", map[string]string{"hash": transactionHash}) if err != nil { - return tss.RPCGetIngestTxResponse{}, fmt.Errorf(err.Error()) + return tss.RPCGetIngestTxResponse{Status: tss.ErrorStatus}, fmt.Errorf(err.Error()) } getIngestTxResponse := tss.RPCGetIngestTxResponse{} if result, ok := rpcResponse["result"].(map[string]interface{}); ok { - if status, exists := result["status"].(tss.RPCTXStatus); exists { - getIngestTxResponse.Status = status + if status, exists := result["status"].(string); exists { + getIngestTxResponse.Status = tss.RPCTXStatus(status) } - if envelopeXdr, exists := result["envelopeXdr"].(string); exists { - getIngestTxResponse.EnvelopeXDR = envelopeXdr + if envelopeXDR, exists := result["envelopeXdr"].(string); exists { + getIngestTxResponse.EnvelopeXDR = envelopeXDR } - if resultXdr, exists := result["resultXdr"].(string); exists { - getIngestTxResponse.ResultXDR = resultXdr + if resultXDR, exists := result["resultXdr"].(string); exists { + getIngestTxResponse.ResultXDR = resultXDR } if createdAt, exists := result["createdAt"].(string); exists { - // we can supress erroneous createdAt errors as this is not an important field - createdAtInt, _ := strconv.ParseInt(createdAt, 10, 64) - getIngestTxResponse.CreatedAt = createdAtInt + createdAtInt, e := strconv.ParseInt(createdAt, 10, 64) + if e != nil { + getIngestTxResponse.Status = tss.ErrorStatus + err = fmt.Errorf("cannot parse createdAt") + } else { + getIngestTxResponse.CreatedAt = createdAtInt + } } + } else { + getIngestTxResponse.Status = tss.ErrorStatus + return getIngestTxResponse, fmt.Errorf("RPC response has no result field") + } - return getIngestTxResponse, nil + return getIngestTxResponse, err } diff --git a/internal/tss/utils/transaction_service_test.go b/internal/tss/utils/transaction_service_test.go index 95060ba..f58d2fd 100644 --- a/internal/tss/utils/transaction_service_test.go +++ b/internal/tss/utils/transaction_service_test.go @@ -5,8 +5,10 @@ import ( "context" "encoding/json" "errors" + "fmt" "io" "net/http" + "strings" "testing" "github.com/stellar/go/clients/horizonclient" @@ -16,44 +18,69 @@ import ( "github.com/stellar/go/xdr" "github.com/stellar/wallet-backend/internal/signing" "github.com/stellar/wallet-backend/internal/tss" - tssErr "github.com/stellar/wallet-backend/internal/tss/errors" + tsserror "github.com/stellar/wallet-backend/internal/tss/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) +func buildTestTransaction() *txnbuild.Transaction { + accountToSponsor := keypair.MustRandom() + + tx, _ := txnbuild.NewTransaction(txnbuild.TransactionParams{ + SourceAccount: &txnbuild.SimpleAccount{ + AccountID: accountToSponsor.Address(), + Sequence: 124, + }, + IncrementSequenceNum: true, + Operations: []txnbuild.Operation{ + &txnbuild.Payment{ + Destination: keypair.MustRandom().Address(), + Amount: "14", + Asset: txnbuild.NativeAsset{}, + }, + }, + BaseFee: 104, + Preconditions: txnbuild.Preconditions{TimeBounds: txnbuild.NewTimeout(10)}, + }) + return tx +} + func TestValidateOptions(t *testing.T) { - t.Run("return_error_when_distribution_signature_client_null", func(t *testing.T) { + t.Run("return_error_when_distribution_signature_client_nil", func(t *testing.T) { opts := TransactionServiceOptions{ DistributionAccountSignatureClient: nil, ChannelAccountSignatureClient: &signing.SignatureClientMock{}, HorizonClient: &horizonclient.MockClient{}, - RpcUrl: "http://localhost:8000/soroban/rpc", + RPCURL: "http://localhost:8000/soroban/rpc", BaseFee: 114, + HTTPClient: &MockHTTPClient{}, } err := opts.ValidateOptions() assert.Equal(t, "distribution account signature client cannot be nil", err.Error()) }) - t.Run("return_error_when_channel_signature_client_null", func(t *testing.T) { + t.Run("return_error_when_channel_signature_client_nil", func(t *testing.T) { opts := TransactionServiceOptions{ DistributionAccountSignatureClient: &signing.SignatureClientMock{}, ChannelAccountSignatureClient: nil, HorizonClient: &horizonclient.MockClient{}, - RpcUrl: "http://localhost:8000/soroban/rpc", + RPCURL: "http://localhost:8000/soroban/rpc", BaseFee: 114, + HTTPClient: &MockHTTPClient{}, } err := opts.ValidateOptions() assert.Equal(t, "channel account signature client cannot be nil", err.Error()) }) - t.Run("return_error_when_horizon_client_null", func(t *testing.T) { + t.Run("return_error_when_horizon_client_nil", func(t *testing.T) { opts := TransactionServiceOptions{ DistributionAccountSignatureClient: &signing.SignatureClientMock{}, ChannelAccountSignatureClient: &signing.SignatureClientMock{}, HorizonClient: nil, - RpcUrl: "http://localhost:8000/soroban/rpc", + RPCURL: "http://localhost:8000/soroban/rpc", BaseFee: 114, + HTTPClient: &MockHTTPClient{}, } err := opts.ValidateOptions() assert.Equal(t, "horizon client cannot be nil", err.Error()) @@ -64,8 +91,9 @@ func TestValidateOptions(t *testing.T) { DistributionAccountSignatureClient: &signing.SignatureClientMock{}, ChannelAccountSignatureClient: &signing.SignatureClientMock{}, HorizonClient: &horizonclient.MockClient{}, - RpcUrl: "", + RPCURL: "", BaseFee: 114, + HTTPClient: &MockHTTPClient{}, } err := opts.ValidateOptions() assert.Equal(t, "rpc url cannot be empty", err.Error()) @@ -76,15 +104,28 @@ func TestValidateOptions(t *testing.T) { DistributionAccountSignatureClient: &signing.SignatureClientMock{}, ChannelAccountSignatureClient: &signing.SignatureClientMock{}, HorizonClient: &horizonclient.MockClient{}, - RpcUrl: "http://localhost:8000/soroban/rpc", + RPCURL: "http://localhost:8000/soroban/rpc", BaseFee: txnbuild.MinBaseFee - 10, + HTTPClient: &MockHTTPClient{}, } err := opts.ValidateOptions() assert.Equal(t, "base fee is lower than the minimum network fee", err.Error()) }) + + t.Run("return_error_http_client_nil", func(t *testing.T) { + opts := TransactionServiceOptions{ + DistributionAccountSignatureClient: &signing.SignatureClientMock{}, + ChannelAccountSignatureClient: &signing.SignatureClientMock{}, + HorizonClient: &horizonclient.MockClient{}, + RPCURL: "http://localhost:8000/soroban/rpc", + BaseFee: 114, + } + err := opts.ValidateOptions() + assert.Equal(t, "http client cannot be nil", err.Error()) + }) } -func TestSignAndBuildNewTransaction(t *testing.T) { +func TestSignAndBuildNewFeeBumpTransaction(t *testing.T) { distributionAccountSignatureClient := signing.SignatureClientMock{} defer distributionAccountSignatureClient.AssertExpectations(t) channelAccountSignatureClient := signing.SignatureClientMock{} @@ -95,17 +136,17 @@ func TestSignAndBuildNewTransaction(t *testing.T) { DistributionAccountSignatureClient: &distributionAccountSignatureClient, ChannelAccountSignatureClient: &channelAccountSignatureClient, HorizonClient: &horizonClient, - RpcUrl: "http://localhost:8000/soroban/rpc", + RPCURL: "http://localhost:8000/soroban/rpc", BaseFee: 114, - Ctx: context.Background(), + HTTPClient: &MockHTTPClient{}, }) - txStr, _ := BuildTestTransaction().Base64() + txStr, _ := buildTestTransaction().Base64() t.Run("malformed_transaction_string", func(t *testing.T) { - feeBumpTx, err := txService.SignAndBuildNewTransaction("abcd") + feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), "abcd") assert.Empty(t, feeBumpTx) - assert.ErrorIs(t, tssErr.OriginalXdrMalformed, err) + assert.ErrorIs(t, tsserror.OriginalXDRMalformed, err) }) t.Run("channel_account_signature_client_get_account_public_key_err", func(t *testing.T) { @@ -114,7 +155,7 @@ func TestSignAndBuildNewTransaction(t *testing.T) { Return("", errors.New("channel accounts unavailable")). Once() - feeBumpTx, err := txService.SignAndBuildNewTransaction(txStr) + feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) assert.Empty(t, feeBumpTx) assert.Equal(t, "getting channel account public key: channel accounts unavailable", err.Error()) }) @@ -133,7 +174,7 @@ func TestSignAndBuildNewTransaction(t *testing.T) { Return(horizon.Account{}, errors.New("horizon down")). Once() - feeBumpTx, err := txService.SignAndBuildNewTransaction(txStr) + feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) assert.Empty(t, feeBumpTx) assert.Equal(t, "getting channel account details from horizon: horizon down", err.Error()) }) @@ -155,7 +196,7 @@ func TestSignAndBuildNewTransaction(t *testing.T) { Return(horizon.Account{AccountID: channelAccount.Address(), Sequence: 1}, nil). Once() - feeBumpTx, err := txService.SignAndBuildNewTransaction(txStr) + feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) assert.Empty(t, feeBumpTx) assert.Equal(t, "signing transaction with channel account: unable to sign", err.Error()) }) @@ -183,14 +224,14 @@ func TestSignAndBuildNewTransaction(t *testing.T) { Return(horizon.Account{AccountID: channelAccount.Address(), Sequence: 1}, nil). Once() - feeBumpTx, err := txService.SignAndBuildNewTransaction(txStr) + feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) assert.Empty(t, feeBumpTx) assert.Equal(t, "getting distribution account public key: client down", err.Error()) }) t.Run("horizon_client_sign_stellar_transaction_w_distribition_account_err", func(t *testing.T) { account := keypair.MustRandom() - signedTx := BuildTestTransaction() + signedTx := buildTestTransaction() channelAccountSignatureClient. On("GetAccountPublicKey", context.Background()). Return(account.Address(), nil). @@ -214,14 +255,14 @@ func TestSignAndBuildNewTransaction(t *testing.T) { Return(horizon.Account{AccountID: account.Address(), Sequence: 1}, nil). Once() - feeBumpTx, err := txService.SignAndBuildNewTransaction(txStr) + feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) assert.Empty(t, feeBumpTx) assert.Equal(t, "signing the fee bump transaction with distribution account: unable to sign", err.Error()) }) t.Run("returns_signed_tx", func(t *testing.T) { account := keypair.MustRandom() - signedTx := BuildTestTransaction() + signedTx := buildTestTransaction() testFeeBumpTx, _ := txnbuild.NewFeeBumpTransaction( txnbuild.FeeBumpTransactionParams{ Inner: signedTx, @@ -252,321 +293,346 @@ func TestSignAndBuildNewTransaction(t *testing.T) { Return(horizon.Account{AccountID: account.Address(), Sequence: 1}, nil). Once() - feeBumpTx, err := txService.SignAndBuildNewTransaction(txStr) + feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) assert.Equal(t, feeBumpTx, testFeeBumpTx) assert.Empty(t, err) }) } -type MockPost struct { - mock.Mock -} +type errorReader struct{} -func (m *MockPost) Post(url string, content string, body io.Reader) (*http.Response, error) { - args := m.Called(url, content, body) - return args.Get(0).(*http.Response), args.Error(1) +func (e *errorReader) Read(p []byte) (n int, err error) { + return 0, fmt.Errorf("read error") } -type MockUnMarshallRPCResponse struct { - mock.Mock +func (e *errorReader) Close() error { + return nil } -func (m *MockUnMarshallRPCResponse) ReadAll(r io.Reader) ([]byte, error) { - args := m.Called(r) - return args.Get(0).(([]byte)), args.Error(1) - -} - -type MockUnMarshalJSON struct { - mock.Mock -} - -func (m *MockUnMarshalJSON) UnMarshalJSONBody(body []byte) (map[string]interface{}, error) { - args := m.Called(body) - if args.Get(0) == nil { - return nil, args.Error(1) - } - return args.Get(0).(map[string]interface{}), args.Error(1) -} - -func TestCallRPC(t *testing.T) { - mockPost := MockPost{} - RpcPost = mockPost.Post - defer func() { RpcPost = http.Post }() - mockUnMarshalRPCResponse := MockUnMarshallRPCResponse{} - UnMarshalRPCResponse = mockUnMarshalRPCResponse.ReadAll - defer func() { UnMarshalRPCResponse = io.ReadAll }() - mockUnMarshalJSON := MockUnMarshalJSON{} - UnMarshalJSON = mockUnMarshalJSON.UnMarshalJSONBody - defer func() { UnMarshalJSON = parseJSONBody }() +func TestSendRPCRequest(t *testing.T) { + mockHTTPClient := MockHTTPClient{} + rpcURL := "http://localhost:8000/soroban/rpc" + txService, _ := NewTransactionService(TransactionServiceOptions{ + DistributionAccountSignatureClient: &signing.SignatureClientMock{}, + ChannelAccountSignatureClient: &signing.SignatureClientMock{}, + HorizonClient: &horizonclient.MockClient{}, + RPCURL: rpcURL, + BaseFee: 114, + HTTPClient: &mockHTTPClient, + }) + method := "sendTransaction" params := map[string]string{"transaction": "ABCD"} payload := map[string]interface{}{ "jsonrpc": "2.0", "id": 1, - "method": "sendTransaction", + "method": method, "params": params, } jsonData, _ := json.Marshal(payload) - rpcUrl := "http://localhost:8000/soroban/rpc" - t.Run("rpc_post_call_fails", func(t *testing.T) { - mockPost. - On("Post", rpcUrl, "application/json", bytes.NewBuffer(jsonData)). - Return(&http.Response{}, errors.New("connection error")). + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(&http.Response{}, errors.New("RPC Connection fail")). Once() - response, err := callRPC(rpcUrl, "sendTransaction", params) + resp, err := txService.sendRPCRequest(method, params) - assert.Empty(t, response) - assert.Equal(t, "sendTransaction: sending POST request to rpc: connection error", err.Error()) + assert.Empty(t, resp) + assert.Equal(t, "sendTransaction: sending POST request to rpc: RPC Connection fail", err.Error()) }) - t.Run("unmarshal_rpc_response_fails", func(t *testing.T) { - mockResponse := &http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(`{"mock": "response"}`)), - } - - mockPost. - On("Post", rpcUrl, "application/json", bytes.NewBuffer(jsonData)). - Return(mockResponse, nil). - Once() - mockUnMarshalRPCResponse. - On("ReadAll", mockResponse.Body). - Return([]byte{}, errors.New("bad string")). + t.Run("unmarshaling_rpc_response_fails", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(&errorReader{}), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). Once() - response, err := callRPC(rpcUrl, "sendTransaction", params) + resp, err := txService.sendRPCRequest(method, params) - assert.Empty(t, response) - assert.Equal(t, "sendTransaction: unmarshalling rpc response: bad string", err.Error()) + assert.Empty(t, resp) + assert.Equal(t, "sendTransaction: unmarshaling RPC response", err.Error()) }) - t.Run("unmarshal_json_fails", func(t *testing.T) { - mockResponse := &http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(`{"mock": "response"}`)), + t.Run("unmarshaling_json_fails", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{invalid-json`)), } - - mockPost. - On("Post", rpcUrl, "application/json", mock.AnythingOfType("*bytes.Buffer")). - Return(mockResponse, nil). - Once() - - body := []byte("response") - mockUnMarshalRPCResponse. - On("ReadAll", mockResponse.Body). - Return(body, nil). + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). Once() - mockUnMarshalJSON. - On("UnMarshalJSONBody", body). - Return(nil, errors.New("bad json format")). - Once() + resp, err := txService.sendRPCRequest(method, params) - response, err := callRPC(rpcUrl, "sendTransaction", params) - - assert.Empty(t, response) - assert.Equal(t, "sendTransaction: parsing rpc response JSON: bad json format", err.Error()) + assert.Empty(t, resp) + assert.Equal(t, "sendTransaction: parsing RPC response JSON", err.Error()) }) - t.Run("returns_unmarshalled_value", func(t *testing.T) { - mockResponse := &http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(`{"mock": "response"}`)), - } - - mockPost. - On("Post", rpcUrl, "application/json", mock.AnythingOfType("*bytes.Buffer")). - Return(mockResponse, nil). - Once() - - body := []byte("response") - mockUnMarshalRPCResponse. - On("ReadAll", mockResponse.Body). - Return(body, nil). - Once() - - expectedResponse := map[string]interface{}{"jsonrpc": "2.0", "id": 1, "result": map[string]interface{}{"status": "SUCCESS", "envelopeXdr": "ABCD"}} - mockUnMarshalJSON. - On("UnMarshalJSONBody", body). - Return(expectedResponse, nil). + t.Run("returns_rpc_response", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"status": "success"}`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). Once() - rpcResponse, err := callRPC(rpcUrl, "sendTransaction", params) + resp, err := txService.sendRPCRequest(method, params) - assert.Equal(t, rpcResponse, expectedResponse) + assert.Equal(t, resp, map[string]interface{}{"status": "success"}) assert.Empty(t, err) }) } -type MockCallRPC struct { - mock.Mock -} - -func (m *MockCallRPC) callRPC(rpcUrl string, method string, params map[string]string) (map[string]interface{}, error) { - args := m.Called(rpcUrl, method, params) - if args.Get(0) == nil { - return nil, args.Error(1) - } - return args.Get(0).(map[string]interface{}), args.Error(1) -} - -type MockUnMarshalErrorResultXdr struct { - mock.Mock -} - -func (m *MockUnMarshalErrorResultXdr) UnMarshalErrorResultXdr(errorResultXdr string) (tss.RPCTXCode, error) { - args := m.Called(errorResultXdr) - return args.Get(0).(tss.RPCTXCode), args.Error(1) -} - func TestSendTransaction(t *testing.T) { - mockCallRPC := MockCallRPC{} - callRPC = mockCallRPC.callRPC - defer func() { callRPC = sendRPCRequest }() - mockUnMarshalErrorResultXdr := MockUnMarshalErrorResultXdr{} - UnMarshalErrorResultXdr = mockUnMarshalErrorResultXdr.UnMarshalErrorResultXdr - defer func() { UnMarshalErrorResultXdr = parseErrorResultXdr }() + mockHTTPClient := MockHTTPClient{} + rpcURL := "http://localhost:8000/soroban/rpc" txService, _ := NewTransactionService(TransactionServiceOptions{ DistributionAccountSignatureClient: &signing.SignatureClientMock{}, ChannelAccountSignatureClient: &signing.SignatureClientMock{}, HorizonClient: &horizonclient.MockClient{}, - RpcUrl: "http://localhost:8000/soroban/rpc", + RPCURL: rpcURL, BaseFee: 114, + HTTPClient: &mockHTTPClient, }) - txXdr, _ := BuildTestTransaction().Base64() - rpcUrl := "http://localhost:8000/soroban/rpc" + method := "sendTransaction" + params := map[string]string{"transaction": "ABCD"} + payload := map[string]interface{}{ + "jsonrpc": "2.0", + "id": 1, + "method": method, + "params": params, + } + jsonData, _ := json.Marshal(payload) - t.Run("call_rpc_returns_error", func(t *testing.T) { - mockCallRPC. - On("callRPC", rpcUrl, "sendTransaction", map[string]string{"transaction": txXdr}). - Return(nil, errors.New("unable to reach rpc server")). + t.Run("rpc_request_fails", func(t *testing.T) { + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(&http.Response{}, errors.New("RPC Connection fail")). Once() - rpcSendTxResponse, err := txService.SendTransaction(txXdr) - assert.Equal(t, rpcSendTxResponse.Code.OtherCodes, tss.RPCFailCode) - assert.Equal(t, "unable to reach rpc server", err.Error()) + resp, err := txService.SendTransaction("ABCD") + + assert.Equal(t, tss.RPCFailCode, resp.Code.OtherCodes) + assert.Equal(t, "sendTransaction: sending POST request to rpc: RPC Connection fail", err.Error()) + }) - t.Run("error_unmarshaling_error_result_xdr", func(t *testing.T) { - errorResultXdr := "AAAAAAAAAGT////7AAAAAA==" - rpcResponse := map[string]interface{}{"jsonrpc": "2.0", "id": 1, "result": map[string]interface{}{"status": tss.ErrorStatus, "errorResultXdr": errorResultXdr}} - mockCallRPC. - On("callRPC", rpcUrl, "sendTransaction", map[string]string{"transaction": txXdr}). - Return(rpcResponse, nil). + + t.Run("response_has_no_result_field", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"foo": "bar"}`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). Once() - mockUnMarshalErrorResultXdr. - On("UnMarshalErrorResultXdr", errorResultXdr). - Return(tss.RPCTXCode{OtherCodes: tss.UnMarshalBinaryCode}, errors.New("unable to unmarshal")). + resp, err := txService.SendTransaction("ABCD") + + assert.Equal(t, tss.RPCFailCode, resp.Code.OtherCodes) + assert.Equal(t, "RPC response has no result field", err.Error()) + + }) + + t.Run("response_has_status_field", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"result": {"status": "PENDING"}}`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). Once() - rpcSendTxResponse, err := txService.SendTransaction(txXdr) - assert.Equal(t, rpcSendTxResponse.Status, tss.ErrorStatus) - assert.Equal(t, rpcSendTxResponse.Code.OtherCodes, tss.UnMarshalBinaryCode) - assert.Equal(t, "unable to unmarshal", err.Error()) - }) - t.Run("return_send_tx_response", func(t *testing.T) { - errorResultXdr := "AAAAAAAAAGT////7AAAAAA==" - rpcResponse := map[string]interface{}{"jsonrpc": "2.0", "id": 1, "result": map[string]interface{}{"status": tss.ErrorStatus, "errorResultXdr": errorResultXdr}} - mockCallRPC. - On("callRPC", rpcUrl, "sendTransaction", map[string]string{"transaction": txXdr}). - Return(rpcResponse, nil). + resp, err := txService.SendTransaction("ABCD") + + assert.Equal(t, tss.PendingStatus, resp.Status) + assert.Empty(t, err) + }) + + t.Run("response_has_hash_field", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"result": {"hash": "xyz"}}`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). + Once() + + resp, err := txService.SendTransaction("ABCD") + + assert.Equal(t, "xyz", resp.TransactionHash) + assert.Empty(t, err) + }) + + t.Run("response_has_unparsable_errorResultXdr", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"result": {"errorResultXdr": "ABC123"}}`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). Once() - mockUnMarshalErrorResultXdr. - On("UnMarshalErrorResultXdr", errorResultXdr). - Return(tss.RPCTXCode{TxResultCode: xdr.TransactionResultCodeTxSuccess}, nil). + resp, err := txService.SendTransaction("ABCD") + + assert.Equal(t, tss.UnMarshalBinaryCode, resp.Code.OtherCodes) + assert.Equal(t, "unable to unmarshal errorResultXdr: ABC123", err.Error()) + }) + t.Run("response_has_errorResultXdr", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"result": {"errorResultXdr": "AAAAAAAAAMj////9AAAAAA=="}}`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). Once() - rpcSendTxResponse, err := txService.SendTransaction(txXdr) - assert.Equal(t, rpcSendTxResponse.Status, tss.ErrorStatus) - assert.Equal(t, rpcSendTxResponse.Code.TxResultCode, xdr.TransactionResultCodeTxSuccess) + resp, err := txService.SendTransaction("ABCD") + + assert.Equal(t, xdr.TransactionResultCodeTxTooLate, resp.Code.TxResultCode) assert.Empty(t, err) }) } func TestGetTransaction(t *testing.T) { - mockCallRPC := MockCallRPC{} - callRPC = mockCallRPC.callRPC - defer func() { callRPC = sendRPCRequest }() + mockHTTPClient := MockHTTPClient{} + rpcURL := "http://localhost:8000/soroban/rpc" txService, _ := NewTransactionService(TransactionServiceOptions{ DistributionAccountSignatureClient: &signing.SignatureClientMock{}, ChannelAccountSignatureClient: &signing.SignatureClientMock{}, HorizonClient: &horizonclient.MockClient{}, - RpcUrl: "http://localhost:8000/soroban/rpc", + RPCURL: rpcURL, BaseFee: 114, + HTTPClient: &mockHTTPClient, }) - txHash, _ := BuildTestTransaction().HashHex("abcd") - rpcUrl := "http://localhost:8000/soroban/rpc" + method := "getTransaction" + params := map[string]string{"hash": "XYZ"} + payload := map[string]interface{}{ + "jsonrpc": "2.0", + "id": 1, + "method": method, + "params": params, + } + jsonData, _ := json.Marshal(payload) + + t.Run("rpc_request_fails", func(t *testing.T) { + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(&http.Response{}, errors.New("RPC Connection fail")). + Once() - t.Run("call_rpc_returns_error", func(t *testing.T) { - mockCallRPC. - On("callRPC", rpcUrl, "getTransaction", map[string]string{"hash": txHash}). - Return(nil, errors.New("unable to reach rpc server")). + resp, err := txService.GetTransaction("XYZ") + + assert.Equal(t, tss.ErrorStatus, resp.Status) + assert.Equal(t, "getTransaction: sending POST request to rpc: RPC Connection fail", err.Error()) + + }) + + t.Run("response_has_no_result_field", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"foo": "bar"}`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). Once() - _, err := txService.GetTransaction(txHash) - assert.Equal(t, "unable to reach rpc server", err.Error()) + resp, err := txService.GetTransaction("XYZ") + + assert.Equal(t, tss.ErrorStatus, resp.Status) + assert.Equal(t, "RPC response has no result field", err.Error()) }) - t.Run("returns_resp_with_status", func(t *testing.T) { - rpcResponse := map[string]interface{}{"jsonrpc": "2.0", "id": 1, "result": map[string]interface{}{"status": tss.SuccessStatus}} - mockCallRPC. - On("callRPC", rpcUrl, "getTransaction", map[string]string{"hash": txHash}). - Return(rpcResponse, nil). + t.Run("response_has_status_field", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"result": {"status": "SUCCESS"}}`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). Once() - getIngestTxResponse, err := txService.GetTransaction(txHash) - assert.Equal(t, getIngestTxResponse.Status, tss.SuccessStatus) - assert.Empty(t, getIngestTxResponse.EnvelopeXDR) - assert.Empty(t, getIngestTxResponse.ResultXDR) - assert.Empty(t, getIngestTxResponse.CreatedAt) + resp, err := txService.GetTransaction("XYZ") + + assert.Equal(t, tss.SuccessStatus, resp.Status) assert.Empty(t, err) }) - t.Run("returns_resp_with_envelope_xdr", func(t *testing.T) { - rpcResponse := map[string]interface{}{"jsonrpc": "2.0", "id": 1, "result": map[string]interface{}{"envelopeXdr": "abcd"}} - mockCallRPC. - On("callRPC", rpcUrl, "getTransaction", map[string]string{"hash": txHash}). - Return(rpcResponse, nil). + t.Run("response_has_envelopeXdr_field", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"result": {"envelopeXdr": "envelopeABCD"}}`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). Once() - getIngestTxResponse, err := txService.GetTransaction(txHash) - assert.Empty(t, getIngestTxResponse.Status) - assert.Equal(t, getIngestTxResponse.EnvelopeXDR, "abcd") - assert.Empty(t, getIngestTxResponse.ResultXDR) - assert.Empty(t, getIngestTxResponse.CreatedAt) + resp, err := txService.GetTransaction("XYZ") + + assert.Equal(t, "envelopeABCD", resp.EnvelopeXDR) assert.Empty(t, err) }) - t.Run("returns_resp_with_result_xdr", func(t *testing.T) { - rpcResponse := map[string]interface{}{"jsonrpc": "2.0", "id": 1, "result": map[string]interface{}{"resultXdr": "abcd"}} - mockCallRPC. - On("callRPC", rpcUrl, "getTransaction", map[string]string{"hash": txHash}). - Return(rpcResponse, nil). + t.Run("response_has_resultXdr_field", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"result": {"resultXdr": "resultABCD"}}`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). Once() - getIngestTxResponse, err := txService.GetTransaction(txHash) - assert.Empty(t, getIngestTxResponse.Status) - assert.Empty(t, getIngestTxResponse.EnvelopeXDR) - assert.Equal(t, getIngestTxResponse.ResultXDR, "abcd") - assert.Empty(t, getIngestTxResponse.CreatedAt) + resp, err := txService.GetTransaction("XYZ") + + assert.Equal(t, "resultABCD", resp.ResultXDR) assert.Empty(t, err) }) - t.Run("returns_resp_with_created_at", func(t *testing.T) { - rpcResponse := map[string]interface{}{"jsonrpc": "2.0", "id": 1, "result": map[string]interface{}{"createdAt": "1234"}} - mockCallRPC. - On("callRPC", rpcUrl, "getTransaction", map[string]string{"hash": txHash}). - Return(rpcResponse, nil). + t.Run("unable_to_parse_createdAt", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"result": {"createdAt": "ABCD"}}`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). + Once() + + resp, err := txService.GetTransaction("XYZ") + + assert.Equal(t, tss.ErrorStatus, resp.Status) + assert.Equal(t, "cannot parse createdAt", err.Error()) + }) + + t.Run("response_has_createdAt_field", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"result": {"createdAt": "1234567"}}`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). Once() - getIngestTxResponse, err := txService.GetTransaction(txHash) - assert.Empty(t, getIngestTxResponse.Status) - assert.Empty(t, getIngestTxResponse.EnvelopeXDR) - assert.Empty(t, getIngestTxResponse.ResultXDR) - assert.Equal(t, getIngestTxResponse.CreatedAt, int64(1234)) + resp, err := txService.GetTransaction("XYZ") + + assert.Equal(t, int64(1234567), resp.CreatedAt) assert.Empty(t, err) }) + } diff --git a/internal/tss/utils/types.go b/internal/tss/utils/types.go deleted file mode 100644 index 1cec932..0000000 --- a/internal/tss/utils/types.go +++ /dev/null @@ -1,13 +0,0 @@ -package utils - -import ( - "github.com/stellar/go/txnbuild" - "github.com/stellar/wallet-backend/internal/tss" -) - -type TransactionService interface { - NetworkPassPhrase() string - SignAndBuildNewTransaction(origTxXdr string) (*txnbuild.FeeBumpTransaction, error) - SendTransaction(transactionXdr string) (tss.RPCSendTxResponse, error) - GetTransaction(transactionHash string) (tss.RPCGetIngestTxResponse, error) -} From c195365593b3049f47d4bdf6530fbdf9bebb1e66 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Thu, 19 Sep 2024 01:32:24 -0700 Subject: [PATCH 037/113] TSS Error Handler Service --- cmd/serve.go | 9 + cmd/utils/global_options.go | 101 ++++ go.mod | 5 +- go.sum | 4 +- internal/serve/serve.go | 92 ++- .../error_handler_service_jitter_channel.go | 91 +++ ...rror_handler_service_non_jitter_channel.go | 81 +++ ...handler_service_non_jitter_channel_test.go | 253 +++++++++ ...ror_service_handler_jitter_channel_test.go | 253 +++++++++ internal/tss/channels/mocks.go | 15 + internal/tss/channels/types.go | 10 + internal/tss/channels/utils.go | 47 ++ internal/tss/channels/utils_test.go | 143 +++++ internal/tss/errors/errors.go | 2 +- internal/tss/mocks.go | 19 + internal/tss/router/mocks.go | 16 + internal/tss/router/router.go | 65 +++ internal/tss/router/router_test.go | 51 ++ .../tss/services/error_handler_service.go | 39 ++ .../services/error_handler_service_test.go | 54 ++ internal/tss/services/mocks.go | 16 + internal/tss/services/types.go | 7 + .../tss/services/webhook_handler_service.go | 19 + internal/tss/store/store.go | 73 +++ internal/tss/store/store_test.go | 96 ++++ internal/tss/types.go | 11 + internal/tss/utils/helpers.go | 39 ++ internal/tss/utils/mocks.go | 50 ++ internal/tss/utils/transaction_service.go | 169 +++--- .../tss/utils/transaction_service_test.go | 526 ++++++++++-------- internal/tss/utils/types.go | 13 - 31 files changed, 2035 insertions(+), 334 deletions(-) create mode 100644 internal/tss/channels/error_handler_service_jitter_channel.go create mode 100644 internal/tss/channels/error_handler_service_non_jitter_channel.go create mode 100644 internal/tss/channels/error_handler_service_non_jitter_channel_test.go create mode 100644 internal/tss/channels/error_service_handler_jitter_channel_test.go create mode 100644 internal/tss/channels/mocks.go create mode 100644 internal/tss/channels/types.go create mode 100644 internal/tss/channels/utils.go create mode 100644 internal/tss/channels/utils_test.go create mode 100644 internal/tss/mocks.go create mode 100644 internal/tss/router/mocks.go create mode 100644 internal/tss/router/router.go create mode 100644 internal/tss/router/router_test.go create mode 100644 internal/tss/services/error_handler_service.go create mode 100644 internal/tss/services/error_handler_service_test.go create mode 100644 internal/tss/services/mocks.go create mode 100644 internal/tss/services/types.go create mode 100644 internal/tss/services/webhook_handler_service.go create mode 100644 internal/tss/store/store.go create mode 100644 internal/tss/store/store_test.go create mode 100644 internal/tss/utils/helpers.go create mode 100644 internal/tss/utils/mocks.go delete mode 100644 internal/tss/utils/types.go diff --git a/cmd/serve.go b/cmd/serve.go index 8738d22..e546036 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -32,6 +32,15 @@ func (c *serveCmd) Command() *cobra.Command { utils.ChannelAccountEncryptionPassphraseOption(&cfg.EncryptionPassphrase), utils.SentryDSNOption(&sentryDSN), utils.StellarEnvironmentOption(&stellarEnvironment), + utils.RPCURLOption(&cfg.RPCURL), + utils.ErrorHandlerServiceJitterChannelBufferSizeOption(&cfg.ErrorHandlerServiceJitterChannelBufferSize), + utils.ErrorHandlerServiceJitterChannelMaxWorkersOption(&cfg.ErrorHandlerServiceJitterChannelMaxWorkers), + utils.ErrorHandlerServiceNonJitterChannelBufferSizeOption(&cfg.ErrorHandlerServiceNonJitterChannelBufferSize), + utils.ErrorHandlerServiceNonJitterChannelMaxWorkersOption(&cfg.ErrorHandlerServiceNonJitterChannelMaxWorkers), + utils.ErrorHandlerServiceJitterChannelMinWaitBtwnRetriesMSOption(&cfg.ErrorHandlerServiceJitterChannelMinWaitBtwnRetriesMS), + utils.ErrorHandlerServiceNonJitterChannelWaitBtwnRetriesMSOption(&cfg.ErrorHandlerServiceNonJitterChannelWaitBtwnRetriesMS), + utils.ErrorHandlerServiceJitterChannelMaxRetriesOptions(&cfg.ErrorHandlerServiceJitterChannelMaxRetries), + utils.ErrorHandlerServiceNonJitterChannelMaxRetriesOption(&cfg.ErrorHandlerServiceNonJitterChannelMaxRetries), { Name: "port", Usage: "Port to listen and serve on", diff --git a/cmd/utils/global_options.go b/cmd/utils/global_options.go index 7b53a67..a283c10 100644 --- a/cmd/utils/global_options.go +++ b/cmd/utils/global_options.go @@ -131,6 +131,107 @@ func DistributionAccountSignatureClientProviderOption(configKey *signing.Signatu } } +func RPCURLOption(configKey *string) *config.ConfigOption { + return &config.ConfigOption{ + Name: "rpc-url", + Usage: "The URL of the RPC Server.", + OptType: types.String, + ConfigKey: configKey, + FlagDefault: "localhost:8080", + Required: true, + } +} + +func ErrorHandlerServiceJitterChannelBufferSizeOption(configKey *int) *config.ConfigOption { + return &config.ConfigOption{ + Name: "error-handler-service-jitter-channel-buffer-size", + Usage: "Set the buffer size of the Error Handler Service Jitter channel.", + OptType: types.Int, + ConfigKey: configKey, + FlagDefault: 100, + Required: true, + } +} + +func ErrorHandlerServiceJitterChannelMaxWorkersOption(configKey *int) *config.ConfigOption { + return &config.ConfigOption{ + Name: "error-handler-service-jitter-channel-max-workers", + Usage: "Set the maximum number of workers for the Error Handler Service Jitter channel.", + OptType: types.Int, + ConfigKey: configKey, + FlagDefault: 10, + Required: true, + } +} + +func ErrorHandlerServiceNonJitterChannelBufferSizeOption(configKey *int) *config.ConfigOption { + return &config.ConfigOption{ + Name: "error-handler-service-non-jitter-channel-buffer-size", + Usage: "Set the buffer size of the Error Handler Service Non Jitter channel.", + OptType: types.Int, + ConfigKey: configKey, + FlagDefault: 100, + Required: true, + } + +} + +func ErrorHandlerServiceNonJitterChannelMaxWorkersOption(configKey *int) *config.ConfigOption { + return &config.ConfigOption{ + Name: "error-handler-service-non-jitter-channel-max-workers", + Usage: "Set the maximum number of workers for the Error Handler Service Non Jitter channel.", + OptType: types.Int, + ConfigKey: configKey, + FlagDefault: 10, + Required: true, + } +} + +func ErrorHandlerServiceJitterChannelMinWaitBtwnRetriesMSOption(configKey *int) *config.ConfigOption { + return &config.ConfigOption{ + Name: "error-handler-service-jitter-channel-min-wait-between-retries", + Usage: "Set the minimum amount of time in ms between retries for the Error Handler Service Jitter channel.", + OptType: types.Int, + ConfigKey: configKey, + FlagDefault: 10, + Required: true, + } +} + +func ErrorHandlerServiceNonJitterChannelWaitBtwnRetriesMSOption(configKey *int) *config.ConfigOption { + return &config.ConfigOption{ + Name: "error-handler-service-non-jitter-channel-wait-between-retries", + Usage: "Set the amount of time in ms between retries for the Error Handler Service Non Jitter channel.", + OptType: types.Int, + ConfigKey: configKey, + FlagDefault: 10, + Required: true, + } +} + +func ErrorHandlerServiceJitterChannelMaxRetriesOptions(configKey *int) *config.ConfigOption { + return &config.ConfigOption{ + Name: "error-handler-service-jitter-channel-max-retries", + Usage: "Set the number of retries for each task in the Error Handler Service Jitter channel.", + OptType: types.Int, + ConfigKey: configKey, + FlagDefault: 10, + Required: true, + } + +} + +func ErrorHandlerServiceNonJitterChannelMaxRetriesOption(configKey *int) *config.ConfigOption { + return &config.ConfigOption{ + Name: "error-handler-service-non-jitter-channel-max-retries", + Usage: "Set the number of retries for each task in the Error Handler Service Non Jitter channel.", + OptType: types.Int, + ConfigKey: configKey, + FlagDefault: 10, + Required: true, + } +} + func AWSOptions(awsRegionConfigKey *string, kmsKeyARN *string, required bool) config.ConfigOptions { awsOpts := config.ConfigOptions{ { diff --git a/go.mod b/go.mod index cbd038c..95c4c3b 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/stellar/wallet-backend go 1.22.0 require ( + github.com/alitto/pond v1.9.2 github.com/aws/aws-sdk-go v1.45.26 github.com/getsentry/sentry-go v0.28.1 github.com/go-chi/chi v4.1.2+incompatible @@ -17,6 +18,8 @@ require ( github.com/stellar/go v0.0.0-20240416222646-fd107948e6c4 github.com/stellar/go-xdr v0.0.0-20231122183749-b53fb00bcac2 github.com/stretchr/testify v1.9.0 + golang.org/x/exp v0.0.0-20231006140011-7918f672742d + golang.org/x/net v0.23.0 golang.org/x/term v0.18.0 ) @@ -87,9 +90,7 @@ require ( go.opentelemetry.io/otel/trace v1.21.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.21.0 // indirect - golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect golang.org/x/mod v0.13.0 // indirect - golang.org/x/net v0.23.0 // indirect golang.org/x/oauth2 v0.16.0 // indirect golang.org/x/sync v0.6.0 // indirect golang.org/x/sys v0.18.0 // indirect diff --git a/go.sum b/go.sum index 50e8280..65eb545 100644 --- a/go.sum +++ b/go.sum @@ -56,6 +56,8 @@ github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migc github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= +github.com/alitto/pond v1.9.2 h1:9Qb75z/scEZVCoSU+osVmQ0I0JOeLfdTDafrbcJ8CLs= +github.com/alitto/pond v1.9.2/go.mod h1:xQn3P/sHTYcU/1BR3i86IGIrilcrGC2LiS+E2+CJWsI= github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= @@ -758,4 +760,4 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= \ No newline at end of file diff --git a/internal/serve/serve.go b/internal/serve/serve.go index ccb3e2a..6c302d0 100644 --- a/internal/serve/serve.go +++ b/internal/serve/serve.go @@ -25,6 +25,12 @@ import ( "github.com/stellar/wallet-backend/internal/signing" "github.com/stellar/wallet-backend/internal/signing/store" signingutils "github.com/stellar/wallet-backend/internal/signing/utils" + "github.com/stellar/wallet-backend/internal/tss" + tsschannel "github.com/stellar/wallet-backend/internal/tss/channels" + tssrouter "github.com/stellar/wallet-backend/internal/tss/router" + tssservices "github.com/stellar/wallet-backend/internal/tss/services" + tssstore "github.com/stellar/wallet-backend/internal/tss/store" + tssutils "github.com/stellar/wallet-backend/internal/tss/utils" ) // NOTE: perhaps move this to a environment variable. @@ -61,6 +67,17 @@ type Configs struct { // Error Tracker AppTracker apptracker.AppTracker + + // TSS + RPCURL string + ErrorHandlerServiceJitterChannelBufferSize int + ErrorHandlerServiceJitterChannelMaxWorkers int + ErrorHandlerServiceNonJitterChannelBufferSize int + ErrorHandlerServiceNonJitterChannelMaxWorkers int + ErrorHandlerServiceJitterChannelMinWaitBtwnRetriesMS int + ErrorHandlerServiceNonJitterChannelWaitBtwnRetriesMS int + ErrorHandlerServiceJitterChannelMaxRetries int + ErrorHandlerServiceNonJitterChannelMaxRetries int } type handlerDeps struct { @@ -75,6 +92,10 @@ type handlerDeps struct { AccountSponsorshipService services.AccountSponsorshipService PaymentService services.PaymentService AppTracker apptracker.AppTracker + + // TSS + ErrorHandlerServiceJitterChannel tss.Channel + ErrorHandlerServiceNonJitterChannel tss.Channel } func Serve(cfg Configs) error { @@ -92,6 +113,8 @@ func Serve(cfg Configs) error { }, OnStopping: func() { log.Info("Stopping Wallet Backend server") + deps.ErrorHandlerServiceJitterChannel.Stop() + deps.ErrorHandlerServiceNonJitterChannel.Stop() }, }) @@ -155,14 +178,69 @@ func initHandlerDeps(cfg Configs) (handlerDeps, error) { } go ensureChannelAccounts(channelAccountService, int64(cfg.NumberOfChannelAccounts)) + // TSS + txServiceOpts := tssutils.TransactionServiceOptions{ + DistributionAccountSignatureClient: cfg.DistributionAccountSignatureClient, + ChannelAccountSignatureClient: cfg.ChannelAccountSignatureClient, + HorizonClient: &horizonClient, + RPCURL: cfg.RPCURL, + BaseFee: int64(cfg.BaseFee), // Reuse horizon base fee for RPC?? + } + tssTxService, err := tssutils.NewTransactionService(txServiceOpts) + + if err != nil { + return handlerDeps{}, fmt.Errorf("instantiating tss transaction service: %w", err) + } + + store := tssstore.NewStore(dbConnectionPool) + + jitterChannelOpts := tsschannel.RPCErrorHandlerServiceJitterChannelConfigs{ + Store: store, + TxService: tssTxService, + MaxBufferSize: cfg.ErrorHandlerServiceJitterChannelBufferSize, + MaxWorkers: cfg.ErrorHandlerServiceJitterChannelMaxWorkers, + MaxRetries: cfg.ErrorHandlerServiceJitterChannelMaxRetries, + MinWaitBtwnRetriesMS: cfg.ErrorHandlerServiceJitterChannelMinWaitBtwnRetriesMS, + } + + jitterChannel := tsschannel.NewErrorHandlerServiceJitterChannel(jitterChannelOpts) + + nonJitterChannelOpts := tsschannel.RPCErrorHandlerServiceNonJitterChannelConfigs{ + Store: store, + TxService: tssTxService, + MaxBufferSize: cfg.ErrorHandlerServiceNonJitterChannelBufferSize, + MaxWorkers: cfg.ErrorHandlerServiceNonJitterChannelMaxWorkers, + MaxRetries: cfg.ErrorHandlerServiceNonJitterChannelMaxRetries, + WaitBtwnRetriesMS: cfg.ErrorHandlerServiceNonJitterChannelWaitBtwnRetriesMS, + } + + nonJitterChannel := tsschannel.NewErrorHandlerServiceNonJitterChannel(nonJitterChannelOpts) + + errHandlerService := tssservices.NewErrorHandlerService(tssservices.ErrorHandlerServiceConfigs{ + JitterChannel: jitterChannel, + NonJitterChannel: nonJitterChannel, + }) + + webhookHandlerService := tssservices.NewWebhookHandlerService(nil) + + router := tssrouter.NewRouter(tssrouter.RouterConfigs{ + ErrorHandlerService: errHandlerService, + WebhookHandlerService: webhookHandlerService, + }) + + jitterChannel.SetRouter(router) + nonJitterChannel.SetRouter(router) + return handlerDeps{ - Models: models, - SignatureVerifier: signatureVerifier, - SupportedAssets: cfg.SupportedAssets, - AccountService: accountService, - AccountSponsorshipService: accountSponsorshipService, - PaymentService: paymentService, - AppTracker: cfg.AppTracker, + Models: models, + SignatureVerifier: signatureVerifier, + SupportedAssets: cfg.SupportedAssets, + AccountService: accountService, + AccountSponsorshipService: accountSponsorshipService, + PaymentService: paymentService, + AppTracker: cfg.AppTracker, + ErrorHandlerServiceJitterChannel: jitterChannel, + ErrorHandlerServiceNonJitterChannel: nonJitterChannel, }, nil } diff --git a/internal/tss/channels/error_handler_service_jitter_channel.go b/internal/tss/channels/error_handler_service_jitter_channel.go new file mode 100644 index 0000000..9c4a5b1 --- /dev/null +++ b/internal/tss/channels/error_handler_service_jitter_channel.go @@ -0,0 +1,91 @@ +package channels + +import ( + "context" + "fmt" + "slices" + "time" + + "github.com/alitto/pond" + "github.com/stellar/go/support/log" + "github.com/stellar/wallet-backend/internal/tss" + "github.com/stellar/wallet-backend/internal/tss/router" + tss_store "github.com/stellar/wallet-backend/internal/tss/store" + "github.com/stellar/wallet-backend/internal/tss/utils" + "golang.org/x/exp/rand" +) + +type RPCErrorHandlerServiceJitterChannelConfigs struct { + Store tss_store.Store + TxService utils.TransactionService + Router router.Router + MaxBufferSize int + MaxWorkers int + MaxRetries int + MinWaitBtwnRetriesMS int +} + +type rpcErrorHandlerServiceJitterPool struct { + Pool *pond.WorkerPool + TxService utils.TransactionService + Store tss_store.Store + Router router.Router + MaxRetries int + MinWaitBtwnRetriesMS int +} + +func jitter(dur time.Duration) time.Duration { + halfDur := int64(dur / 2) + delta := rand.Int63n(halfDur) - halfDur/2 + return dur + time.Duration(delta) +} + +func NewErrorHandlerServiceJitterChannel(cfg RPCErrorHandlerServiceJitterChannelConfigs) *rpcErrorHandlerServiceJitterPool { + pool := pond.New(cfg.MaxBufferSize, cfg.MaxWorkers, pond.Strategy(pond.Balanced())) + return &rpcErrorHandlerServiceJitterPool{ + Pool: pool, + TxService: cfg.TxService, + Store: cfg.Store, + MaxRetries: cfg.MaxRetries, + MinWaitBtwnRetriesMS: cfg.MinWaitBtwnRetriesMS, + } +} + +func (p *rpcErrorHandlerServiceJitterPool) Send(payload tss.Payload) { + p.Pool.Submit(func() { + p.Receive(payload) + }) +} + +func (p *rpcErrorHandlerServiceJitterPool) Receive(payload tss.Payload) { + ctx := context.Background() + var i int + for i = 0; i < p.MaxRetries; i++ { + fmt.Println(i) + currentBackoff := p.MinWaitBtwnRetriesMS * (1 << i) + sleep(jitter(time.Duration(currentBackoff)) * time.Microsecond) + rpcSendResp, err := SignAndSubmitTransaction(ctx, "ErrorHandlerServiceJitterChannel", payload, p.Store, p.TxService) + if err != nil { + log.Errorf(err.Error()) + return + } + payload.RpcSubmitTxResponse = rpcSendResp + if !slices.Contains(tss.JitterErrorCodes, rpcSendResp.Code.TxResultCode) { + p.Router.Route(payload) + return + } + } + if i == p.MaxRetries { + // Retry limit reached, route the payload to the router so it can re-route it to this pool and keep re-trying + // NOTE: Is this a good idea? Infinite tries per transaction ? + p.Router.Route(payload) + } +} + +func (p *rpcErrorHandlerServiceJitterPool) SetRouter(router router.Router) { + p.Router = router +} + +func (p *rpcErrorHandlerServiceJitterPool) Stop() { + p.Pool.StopAndWait() +} diff --git a/internal/tss/channels/error_handler_service_non_jitter_channel.go b/internal/tss/channels/error_handler_service_non_jitter_channel.go new file mode 100644 index 0000000..1bb9469 --- /dev/null +++ b/internal/tss/channels/error_handler_service_non_jitter_channel.go @@ -0,0 +1,81 @@ +package channels + +import ( + "context" + "slices" + "time" + + "github.com/alitto/pond" + "github.com/stellar/go/support/log" + "github.com/stellar/wallet-backend/internal/tss" + "github.com/stellar/wallet-backend/internal/tss/router" + tss_store "github.com/stellar/wallet-backend/internal/tss/store" + "github.com/stellar/wallet-backend/internal/tss/utils" +) + +type RPCErrorHandlerServiceNonJitterChannelConfigs struct { + Store tss_store.Store + TxService utils.TransactionService + Router router.Router + MaxBufferSize int + MaxWorkers int + MaxRetries int + WaitBtwnRetriesMS int +} + +type rpcErrorHandlerServiceNonJitterPool struct { + Pool *pond.WorkerPool + TxService utils.TransactionService + Store tss_store.Store + Router router.Router + MaxRetries int + WaitBtwnRetriesMS int +} + +func NewErrorHandlerServiceNonJitterChannel(cfg RPCErrorHandlerServiceNonJitterChannelConfigs) *rpcErrorHandlerServiceNonJitterPool { + pool := pond.New(cfg.MaxBufferSize, cfg.MaxWorkers, pond.Strategy(pond.Balanced())) + return &rpcErrorHandlerServiceNonJitterPool{ + Pool: pool, + TxService: cfg.TxService, + Store: cfg.Store, + MaxRetries: cfg.MaxRetries, + WaitBtwnRetriesMS: cfg.WaitBtwnRetriesMS, + } +} + +func (p *rpcErrorHandlerServiceNonJitterPool) Send(payload tss.Payload) { + p.Pool.Submit(func() { + p.Receive(payload) + }) +} + +func (p *rpcErrorHandlerServiceNonJitterPool) Receive(payload tss.Payload) { + ctx := context.Background() + var i int + for i = 0; i < p.MaxRetries; i++ { + sleep(time.Duration(p.WaitBtwnRetriesMS) * time.Microsecond) + rpcSendResp, err := SignAndSubmitTransaction(ctx, "ErrorHandlerServiceNonJitterChannel", payload, p.Store, p.TxService) + if err != nil { + log.Errorf(err.Error()) + return + } + payload.RpcSubmitTxResponse = rpcSendResp + if !slices.Contains(tss.NonJitterErrorCodes, rpcSendResp.Code.TxResultCode) { + p.Router.Route(payload) + return + } + } + if i == p.MaxRetries { + // Retry limit reached, route the payload to the router so it can re-route it to this pool and keep re-trying + // NOTE: Is this a good idea? + p.Router.Route(payload) + } +} + +func (p *rpcErrorHandlerServiceNonJitterPool) SetRouter(router router.Router) { + p.Router = router +} + +func (p *rpcErrorHandlerServiceNonJitterPool) Stop() { + p.Pool.StopAndWait() +} diff --git a/internal/tss/channels/error_handler_service_non_jitter_channel_test.go b/internal/tss/channels/error_handler_service_non_jitter_channel_test.go new file mode 100644 index 0000000..daa0461 --- /dev/null +++ b/internal/tss/channels/error_handler_service_non_jitter_channel_test.go @@ -0,0 +1,253 @@ +package channels + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/stellar/go/xdr" + "github.com/stellar/wallet-backend/internal/db" + "github.com/stellar/wallet-backend/internal/db/dbtest" + "github.com/stellar/wallet-backend/internal/tss" + "github.com/stellar/wallet-backend/internal/tss/router" + "github.com/stellar/wallet-backend/internal/tss/store" + "github.com/stellar/wallet-backend/internal/tss/utils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestNonJitterSend(t *testing.T) { + dbt := dbtest.Open(t) + defer dbt.Close() + + dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) + require.NoError(t, err) + defer dbConnectionPool.Close() + store := store.NewStore(dbConnectionPool) + txServiceMock := utils.TransactionServiceMock{} + cfg := RPCErrorHandlerServiceNonJitterChannelConfigs{ + Store: store, + TxService: &txServiceMock, + MaxBufferSize: 1, + MaxWorkers: 1, + MaxRetries: 3, + WaitBtwnRetriesMS: 10, + } + channel := NewErrorHandlerServiceNonJitterChannel(cfg) + + payload := tss.Payload{} + payload.WebhookURL = "www.stellar.com" + payload.TransactionHash = "hash" + payload.TransactionXDR = "xdr" + txServiceMock. + On("SignAndBuildNewFeeBumpTransaction", payload.TransactionXDR). + Return(nil, errors.New("signing failed")) + + _ = store.UpsertTransaction(context.Background(), payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.NewStatus) + + channel.Send(payload) + channel.Stop() + + var status string + err = dbConnectionPool.GetContext(context.Background(), &status, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) + require.NoError(t, err) + assert.Equal(t, status, string(tss.NewStatus)) +} + +func TestNonJitterReceive(t *testing.T) { + dbt := dbtest.Open(t) + defer dbt.Close() + + dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) + require.NoError(t, err) + defer dbConnectionPool.Close() + store := store.NewStore(dbConnectionPool) + txServiceMock := utils.TransactionServiceMock{} + cfg := RPCErrorHandlerServiceNonJitterChannelConfigs{ + Store: store, + TxService: &txServiceMock, + MaxBufferSize: 1, + MaxWorkers: 1, + MaxRetries: 3, + WaitBtwnRetriesMS: 10, + } + channel := NewErrorHandlerServiceNonJitterChannel(cfg) + + // mock out the sleep function (time.Sleep) so we can check the args it was called with + mockSleep := MockSleep{} + defer mockSleep.AssertExpectations(t) + sleep = mockSleep.Sleep + defer func() { + sleep = time.Sleep + }() + + mockRouter := router.MockRouter{} + defer mockRouter.AssertExpectations(t) + channel.SetRouter(&mockRouter) + networkPass := "passphrase" + feeBumpTx := utils.BuildTestFeeBumpTransaction() + feeBumpTxXDR, _ := feeBumpTx.Base64() + feeBumpTxHash, _ := feeBumpTx.HashHex(networkPass) + payload := tss.Payload{} + payload.WebhookURL = "www.stellar.com" + payload.TransactionHash = "hash" + payload.TransactionXDR = "xdr" + + _ = store.UpsertTransaction(context.Background(), payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.NewStatus) + + t.Run("signing_and_submitting_tx_fails", func(t *testing.T) { + txServiceMock. + On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). + Return(nil, errors.New("sign tx failed")). + Once() + + mockSleep. + On("Sleep", time.Duration(time.Duration(channel.WaitBtwnRetriesMS)*time.Microsecond)). + Return(). + Once() + + channel.Receive(payload) + + var txStatus string + err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) + require.NoError(t, err) + assert.Equal(t, string(tss.NewStatus), txStatus) + + }) + t.Run("payload_gets_routed", func(t *testing.T) { + sendResp := tss.RPCSendTxResponse{} + sendResp.Status = tss.TryAgainLaterStatus + sendResp.TransactionHash = feeBumpTxHash + sendResp.TransactionXDR = feeBumpTxXDR + sendResp.Code.TxResultCode = xdr.TransactionResultCodeTxInsufficientFee + txServiceMock. + On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). + Return(feeBumpTx, nil). + Once(). + On("NetworkPassphrase"). + Return(networkPass). + Once(). + On("SendTransaction", feeBumpTxXDR). + Return(sendResp, nil). + Once() + + mockSleep. + On("Sleep", time.Duration(time.Duration(channel.WaitBtwnRetriesMS)*time.Microsecond)). + Return(). + Once() + + mockRouter. + On("Route", mock.AnythingOfType("tss.Payload")). + Return(). + Once() + + channel.Receive(payload) + + var txStatus string + err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) + require.NoError(t, err) + assert.Equal(t, string(tss.TryAgainLaterStatus), txStatus) + + var tryStatus int + err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) + require.NoError(t, err) + assert.Equal(t, int(xdr.TransactionResultCodeTxInsufficientFee), tryStatus) + }) + + t.Run("retries", func(t *testing.T) { + sendResp1 := tss.RPCSendTxResponse{} + sendResp1.Status = tss.ErrorStatus + sendResp1.TransactionHash = feeBumpTxHash + sendResp1.TransactionXDR = feeBumpTxXDR + sendResp1.Code.TxResultCode = xdr.TransactionResultCodeTxTooEarly + + sendResp2 := tss.RPCSendTxResponse{} + sendResp2.Status = tss.TryAgainLaterStatus + sendResp2.TransactionHash = feeBumpTxHash + sendResp2.TransactionXDR = feeBumpTxXDR + sendResp2.Code.TxResultCode = xdr.TransactionResultCodeTxInsufficientFee + txServiceMock. + On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). + Return(feeBumpTx, nil). + Twice(). + On("NetworkPassphrase"). + Return(networkPass). + Twice() + + txServiceMock. + On("SendTransaction", feeBumpTxXDR). + Return(sendResp1, nil). + Once() + + txServiceMock. + On("SendTransaction", feeBumpTxXDR). + Return(sendResp2, nil). + Once() + + mockRouter. + On("Route", mock.AnythingOfType("tss.Payload")). + Return(). + Once() + + mockSleep. + On("Sleep", time.Duration(time.Duration(channel.WaitBtwnRetriesMS)*time.Microsecond)). + Return(). + Twice() + + channel.Receive(payload) + + var txStatus string + err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) + require.NoError(t, err) + assert.Equal(t, string(tss.TryAgainLaterStatus), txStatus) + + var tryStatus int + err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) + require.NoError(t, err) + assert.Equal(t, int(xdr.TransactionResultCodeTxInsufficientFee), tryStatus) + }) + + t.Run("max_retries", func(t *testing.T) { + sendResp := tss.RPCSendTxResponse{} + sendResp.Status = tss.ErrorStatus + sendResp.TransactionHash = feeBumpTxHash + sendResp.TransactionXDR = feeBumpTxXDR + sendResp.Code.TxResultCode = xdr.TransactionResultCodeTxTooEarly + txServiceMock. + On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). + Return(feeBumpTx, nil). + Times(3). + On("NetworkPassphrase"). + Return(networkPass). + Times(3) + + txServiceMock. + On("SendTransaction", feeBumpTxXDR). + Return(sendResp, nil). + Times(3) + + mockRouter. + On("Route", mock.AnythingOfType("tss.Payload")). + Return(). + Once() + + mockSleep. + On("Sleep", time.Duration(time.Duration(channel.WaitBtwnRetriesMS)*time.Microsecond)). + Return(). + Times(3) + + channel.Receive(payload) + + var txStatus string + err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) + require.NoError(t, err) + assert.Equal(t, string(tss.ErrorStatus), txStatus) + + var tryStatus int + err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) + require.NoError(t, err) + assert.Equal(t, int(xdr.TransactionResultCodeTxTooEarly), tryStatus) + }) +} diff --git a/internal/tss/channels/error_service_handler_jitter_channel_test.go b/internal/tss/channels/error_service_handler_jitter_channel_test.go new file mode 100644 index 0000000..13ef183 --- /dev/null +++ b/internal/tss/channels/error_service_handler_jitter_channel_test.go @@ -0,0 +1,253 @@ +package channels + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/stellar/go/xdr" + "github.com/stellar/wallet-backend/internal/db" + "github.com/stellar/wallet-backend/internal/db/dbtest" + "github.com/stellar/wallet-backend/internal/tss" + "github.com/stellar/wallet-backend/internal/tss/router" + "github.com/stellar/wallet-backend/internal/tss/store" + "github.com/stellar/wallet-backend/internal/tss/utils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestJitterSend(t *testing.T) { + dbt := dbtest.Open(t) + defer dbt.Close() + + dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) + require.NoError(t, err) + defer dbConnectionPool.Close() + store := store.NewStore(dbConnectionPool) + txServiceMock := utils.TransactionServiceMock{} + cfg := RPCErrorHandlerServiceJitterChannelConfigs{ + Store: store, + TxService: &txServiceMock, + MaxBufferSize: 1, + MaxWorkers: 1, + MaxRetries: 3, + MinWaitBtwnRetriesMS: 10, + } + channel := NewErrorHandlerServiceJitterChannel(cfg) + + payload := tss.Payload{} + payload.WebhookURL = "www.stellar.com" + payload.TransactionHash = "hash" + payload.TransactionXDR = "xdr" + txServiceMock. + On("SignAndBuildNewFeeBumpTransaction", payload.TransactionXDR). + Return(nil, errors.New("signing failed")) + + _ = store.UpsertTransaction(context.Background(), payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.NewStatus) + + channel.Send(payload) + channel.Stop() + + var status string + err = dbConnectionPool.GetContext(context.Background(), &status, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) + require.NoError(t, err) + assert.Equal(t, status, string(tss.NewStatus)) +} + +func TestJitterReceive(t *testing.T) { + dbt := dbtest.Open(t) + defer dbt.Close() + + dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) + require.NoError(t, err) + defer dbConnectionPool.Close() + store := store.NewStore(dbConnectionPool) + txServiceMock := utils.TransactionServiceMock{} + cfg := RPCErrorHandlerServiceJitterChannelConfigs{ + Store: store, + TxService: &txServiceMock, + MaxBufferSize: 1, + MaxWorkers: 1, + MaxRetries: 3, + MinWaitBtwnRetriesMS: 10, + } + channel := NewErrorHandlerServiceJitterChannel(cfg) + + // mock out the sleep function (time.Sleep) so we can check the args it was called with + mockSleep := MockSleep{} + defer mockSleep.AssertExpectations(t) + sleep = mockSleep.Sleep + defer func() { + sleep = time.Sleep + }() + + mockRouter := router.MockRouter{} + defer mockRouter.AssertExpectations(t) + channel.SetRouter(&mockRouter) + networkPass := "passphrase" + feeBumpTx := utils.BuildTestFeeBumpTransaction() + feeBumpTxXDR, _ := feeBumpTx.Base64() + feeBumpTxHash, _ := feeBumpTx.HashHex(networkPass) + payload := tss.Payload{} + payload.WebhookURL = "www.stellar.com" + payload.TransactionHash = "hash" + payload.TransactionXDR = "xdr" + + _ = store.UpsertTransaction(context.Background(), payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.NewStatus) + + t.Run("signing_and_submitting_tx_fails", func(t *testing.T) { + txServiceMock. + On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). + Return(nil, errors.New("sign tx failed")). + Once() + + mockSleep. + On("Sleep", mock.AnythingOfType("time.Duration")). + Return(). + Once() + + channel.Receive(payload) + + var txStatus string + err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) + require.NoError(t, err) + assert.Equal(t, string(tss.NewStatus), txStatus) + }) + + t.Run("payload_gets_routed", func(t *testing.T) { + sendResp := tss.RPCSendTxResponse{} + sendResp.Status = tss.ErrorStatus + sendResp.TransactionHash = feeBumpTxHash + sendResp.TransactionXDR = feeBumpTxXDR + sendResp.Code.TxResultCode = xdr.TransactionResultCodeTxTooEarly + txServiceMock. + On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). + Return(feeBumpTx, nil). + Once(). + On("NetworkPassphrase"). + Return(networkPass). + Once(). + On("SendTransaction", feeBumpTxXDR). + Return(sendResp, nil). + Once() + + mockSleep. + On("Sleep", mock.AnythingOfType("time.Duration")). + Return(). + Once() + + mockRouter. + On("Route", mock.AnythingOfType("tss.Payload")). + Return(). + Once() + + channel.Receive(payload) + + var txStatus string + err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) + require.NoError(t, err) + assert.Equal(t, string(tss.ErrorStatus), txStatus) + + var tryStatus int + err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) + require.NoError(t, err) + assert.Equal(t, int(xdr.TransactionResultCodeTxTooEarly), tryStatus) + }) + + t.Run("retries", func(t *testing.T) { + sendResp1 := tss.RPCSendTxResponse{} + sendResp1.Status = tss.ErrorStatus + sendResp1.TransactionHash = feeBumpTxHash + sendResp1.TransactionXDR = feeBumpTxXDR + sendResp1.Code.TxResultCode = xdr.TransactionResultCodeTxInsufficientFee + + sendResp2 := tss.RPCSendTxResponse{} + sendResp2.Status = tss.FailedStatus + sendResp2.TransactionHash = feeBumpTxHash + sendResp2.TransactionXDR = feeBumpTxXDR + sendResp2.Code.TxResultCode = xdr.TransactionResultCodeTxFailed + txServiceMock. + On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). + Return(feeBumpTx, nil). + Twice(). + On("NetworkPassphrase"). + Return(networkPass). + Twice() + + txServiceMock. + On("SendTransaction", feeBumpTxXDR). + Return(sendResp1, nil). + Once() + + txServiceMock. + On("SendTransaction", feeBumpTxXDR). + Return(sendResp2, nil). + Once() + + mockRouter. + On("Route", mock.AnythingOfType("tss.Payload")). + Return(). + Once() + + mockSleep. + On("Sleep", mock.AnythingOfType("time.Duration")). + Return(). + Twice() + + channel.Receive(payload) + + var txStatus string + err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) + require.NoError(t, err) + assert.Equal(t, string(tss.FailedStatus), txStatus) + + var tryStatus int + err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) + require.NoError(t, err) + assert.Equal(t, int(xdr.TransactionResultCodeTxFailed), tryStatus) + }) + + t.Run("max_retries", func(t *testing.T) { + sendResp := tss.RPCSendTxResponse{} + sendResp.Status = tss.ErrorStatus + sendResp.TransactionHash = feeBumpTxHash + sendResp.TransactionXDR = feeBumpTxXDR + sendResp.Code.TxResultCode = xdr.TransactionResultCodeTxInsufficientFee + txServiceMock. + On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). + Return(feeBumpTx, nil). + Times(3). + On("NetworkPassphrase"). + Return(networkPass). + Times(3) + + txServiceMock. + On("SendTransaction", feeBumpTxXDR). + Return(sendResp, nil). + Times(3) + + mockRouter. + On("Route", mock.AnythingOfType("tss.Payload")). + Return(). + Once() + + mockSleep. + On("Sleep", mock.AnythingOfType("time.Duration")). + Return(). + Times(3) + + channel.Receive(payload) + + var txStatus string + err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) + require.NoError(t, err) + assert.Equal(t, string(tss.ErrorStatus), txStatus) + + var tryStatus int + err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) + require.NoError(t, err) + assert.Equal(t, int(xdr.TransactionResultCodeTxInsufficientFee), tryStatus) + }) +} diff --git a/internal/tss/channels/mocks.go b/internal/tss/channels/mocks.go new file mode 100644 index 0000000..d6ddc0b --- /dev/null +++ b/internal/tss/channels/mocks.go @@ -0,0 +1,15 @@ +package channels + +import ( + "time" + + "github.com/stretchr/testify/mock" +) + +type MockSleep struct { + mock.Mock +} + +func (m *MockSleep) Sleep(d time.Duration) { + m.Called(d) +} diff --git a/internal/tss/channels/types.go b/internal/tss/channels/types.go new file mode 100644 index 0000000..98bbb17 --- /dev/null +++ b/internal/tss/channels/types.go @@ -0,0 +1,10 @@ +package channels + +import "time" + +var sleep = time.Sleep + +type WorkerPool interface { + Submit(task func()) + StopAndWait() +} diff --git a/internal/tss/channels/utils.go b/internal/tss/channels/utils.go new file mode 100644 index 0000000..4f87ff2 --- /dev/null +++ b/internal/tss/channels/utils.go @@ -0,0 +1,47 @@ +package channels + +import ( + "fmt" + + "github.com/stellar/wallet-backend/internal/tss" + "github.com/stellar/wallet-backend/internal/tss/store" + "github.com/stellar/wallet-backend/internal/tss/utils" + "golang.org/x/net/context" +) + +func SignAndSubmitTransaction(ctx context.Context, channelName string, payload tss.Payload, store store.Store, txService utils.TransactionService) (tss.RPCSendTxResponse, error) { + feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(ctx, payload.TransactionXDR) + if err != nil { + fmt.Println("JERE") + return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to sign/build transaction: %s", channelName, err.Error()) + } + feeBumpTxHash, err := feeBumpTx.HashHex(txService.NetworkPassphrase()) + if err != nil { + return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to hashhex fee bump transaction: %s", channelName, err.Error()) + } + + feeBumpTxXDR, err := feeBumpTx.Base64() + if err != nil { + return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to base64 fee bump transaction: %s", channelName, err.Error()) + } + + err = store.UpsertTry(ctx, payload.TransactionHash, feeBumpTxHash, feeBumpTxXDR, tss.RPCTXCode{OtherCodes: tss.NewCode}) + if err != nil { + return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to upsert try in tries table: %s", channelName, err.Error()) + } + rpcSendResp, rpcErr := txService.SendTransaction(feeBumpTxXDR) + + err = store.UpsertTry(ctx, payload.TransactionHash, feeBumpTxHash, feeBumpTxXDR, rpcSendResp.Code) + if err != nil { + return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to upsert try in tries table: %s", channelName, err.Error()) + } + if rpcErr != nil && rpcSendResp.Code.OtherCodes == tss.RPCFailCode || rpcSendResp.Code.OtherCodes == tss.UnMarshalBinaryCode { + return tss.RPCSendTxResponse{}, fmt.Errorf("%s: RPC fail: %s", channelName, rpcErr.Error()) + } + + err = store.UpsertTransaction(ctx, payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, rpcSendResp.Status) + if err != nil { + return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to do the final update of tx in the transactions table: %s", channelName, err.Error()) + } + return rpcSendResp, nil +} diff --git a/internal/tss/channels/utils_test.go b/internal/tss/channels/utils_test.go new file mode 100644 index 0000000..62c2dba --- /dev/null +++ b/internal/tss/channels/utils_test.go @@ -0,0 +1,143 @@ +package channels + +import ( + "context" + "errors" + "testing" + + "github.com/stellar/go/xdr" + "github.com/stellar/wallet-backend/internal/db" + "github.com/stellar/wallet-backend/internal/db/dbtest" + "github.com/stellar/wallet-backend/internal/tss" + "github.com/stellar/wallet-backend/internal/tss/store" + "github.com/stellar/wallet-backend/internal/tss/utils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestSignAndSubmitTransaction(t *testing.T) { + dbt := dbtest.Open(t) + defer dbt.Close() + + dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) + require.NoError(t, err) + defer dbConnectionPool.Close() + store := store.NewStore(dbConnectionPool) + txServiceMock := utils.TransactionServiceMock{} + networkPass := "passphrase" + feeBumpTx := utils.BuildTestFeeBumpTransaction() + feeBumpTxXDR, _ := feeBumpTx.Base64() + feeBumpTxHash, _ := feeBumpTx.HashHex(networkPass) + payload := tss.Payload{} + payload.WebhookURL = "www.stellar.com" + payload.TransactionHash = "hash" + payload.TransactionXDR = "xdr" + + _ = store.UpsertTransaction(context.Background(), payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.NewStatus) + t.Run("fail_on_tx_build_and_sign", func(t *testing.T) { + txServiceMock. + On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). + Return(nil, errors.New("signing failed")). + Once() + + _, err := SignAndSubmitTransaction(context.Background(), "channel", payload, store, &txServiceMock) + + assert.Equal(t, "channel: Unable to sign/build transaction: signing failed", err.Error()) + + var status string + err = dbConnectionPool.GetContext(context.Background(), &status, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) + require.NoError(t, err) + assert.Equal(t, string(tss.NewStatus), status) + }) + + t.Run("rpc_call_fail", func(t *testing.T) { + sendResp := tss.RPCSendTxResponse{} + sendResp.Code.OtherCodes = tss.RPCFailCode + txServiceMock. + On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). + Return(feeBumpTx, nil). + Once(). + On("NetworkPassphrase"). + Return(networkPass). + Once(). + On("SendTransaction", feeBumpTxXDR). + Return(sendResp, errors.New("RPC Fail")). + Once() + + _, err := SignAndSubmitTransaction(context.Background(), "channel", payload, store, &txServiceMock) + + assert.Equal(t, "channel: RPC fail: RPC Fail", err.Error()) + + var txStatus string + err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) + require.NoError(t, err) + assert.Equal(t, txStatus, string(tss.NewStatus)) + + var tryStatus int + err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) + require.NoError(t, err) + assert.Equal(t, int(tss.RPCFailCode), tryStatus) + }) + + t.Run("rpc_resp_unmarshaling_error", func(t *testing.T) { + sendResp := tss.RPCSendTxResponse{} + sendResp.Code.OtherCodes = tss.UnMarshalBinaryCode + txServiceMock. + On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). + Return(feeBumpTx, nil). + Once(). + On("NetworkPassphrase"). + Return(networkPass). + Once(). + On("SendTransaction", feeBumpTxXDR). + Return(sendResp, errors.New("unable to unmarshal")). + Once() + + _, err := SignAndSubmitTransaction(context.Background(), "channel", payload, store, &txServiceMock) + + assert.Equal(t, "channel: RPC fail: unable to unmarshal", err.Error()) + + var txStatus string + err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) + require.NoError(t, err) + assert.Equal(t, txStatus, string(tss.NewStatus)) + + var tryStatus int + err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) + require.NoError(t, err) + assert.Equal(t, int(tss.UnMarshalBinaryCode), tryStatus) + }) + t.Run("rpc_returns_response", func(t *testing.T) { + sendResp := tss.RPCSendTxResponse{} + sendResp.Status = tss.TryAgainLaterStatus + sendResp.TransactionHash = feeBumpTxHash + sendResp.TransactionXDR = feeBumpTxXDR + sendResp.Code.TxResultCode = xdr.TransactionResultCodeTxInsufficientFee + txServiceMock. + On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). + Return(feeBumpTx, nil). + Once(). + On("NetworkPassphrase"). + Return(networkPass). + Once(). + On("SendTransaction", feeBumpTxXDR). + Return(sendResp, nil). + Once() + + resp, err := SignAndSubmitTransaction(context.Background(), "channel", payload, store, &txServiceMock) + + assert.Equal(t, tss.TryAgainLaterStatus, resp.Status) + assert.Equal(t, xdr.TransactionResultCodeTxInsufficientFee, resp.Code.TxResultCode) + assert.Empty(t, err) + + var txStatus string + err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) + require.NoError(t, err) + assert.Equal(t, string(tss.TryAgainLaterStatus), txStatus) + + var tryStatus int + err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) + require.NoError(t, err) + assert.Equal(t, int(xdr.TransactionResultCodeTxInsufficientFee), tryStatus) + }) +} diff --git a/internal/tss/errors/errors.go b/internal/tss/errors/errors.go index 8cb3bc9..69f8bc3 100644 --- a/internal/tss/errors/errors.go +++ b/internal/tss/errors/errors.go @@ -5,5 +5,5 @@ import ( ) var ( - OriginalXdrMalformed = errors.New("transaction string is malformed") + OriginalXDRMalformed = errors.New("transaction string is malformed") ) diff --git a/internal/tss/mocks.go b/internal/tss/mocks.go new file mode 100644 index 0000000..beb0128 --- /dev/null +++ b/internal/tss/mocks.go @@ -0,0 +1,19 @@ +package tss + +import "github.com/stretchr/testify/mock" + +type MockChannel struct { + mock.Mock +} + +func (m *MockChannel) Send(payload Payload) { + m.Called(payload) +} + +func (m *MockChannel) Receive(payload Payload) { + m.Called(payload) +} + +func (m *MockChannel) Stop() { + m.Called() +} diff --git a/internal/tss/router/mocks.go b/internal/tss/router/mocks.go new file mode 100644 index 0000000..3f4406c --- /dev/null +++ b/internal/tss/router/mocks.go @@ -0,0 +1,16 @@ +package router + +import ( + "github.com/stellar/wallet-backend/internal/tss" + "github.com/stretchr/testify/mock" +) + +type MockRouter struct { + mock.Mock +} + +var _ Router = (*MockRouter)(nil) + +func (r *MockRouter) Route(payload tss.Payload) { + r.Called(payload) +} diff --git a/internal/tss/router/router.go b/internal/tss/router/router.go new file mode 100644 index 0000000..e0d3945 --- /dev/null +++ b/internal/tss/router/router.go @@ -0,0 +1,65 @@ +package router + +import ( + "slices" + + "github.com/stellar/go/xdr" + "github.com/stellar/wallet-backend/internal/tss" + "github.com/stellar/wallet-backend/internal/tss/services" +) + +type Router interface { + Route(payload tss.Payload) +} + +type RouterConfigs struct { + ErrorHandlerService services.Service + WebhookHandlerService services.Service +} + +type router struct { + ErrorHandlerService services.Service + WebhookHandlerService services.Service +} + +var _ Router = (*router)(nil) + +var FinalErrorCodes = []xdr.TransactionResultCode{ + xdr.TransactionResultCodeTxSuccess, + xdr.TransactionResultCodeTxFailed, + xdr.TransactionResultCodeTxMissingOperation, + xdr.TransactionResultCodeTxInsufficientBalance, + xdr.TransactionResultCodeTxBadAuthExtra, + xdr.TransactionResultCodeTxMalformed, +} + +var RetryErrorCodes = []xdr.TransactionResultCode{ + xdr.TransactionResultCodeTxTooLate, + xdr.TransactionResultCodeTxInsufficientFee, + xdr.TransactionResultCodeTxInternalError, + xdr.TransactionResultCodeTxBadSeq, +} + +func NewRouter(cfg RouterConfigs) Router { + return &router{ + ErrorHandlerService: cfg.ErrorHandlerService, + WebhookHandlerService: cfg.WebhookHandlerService, + } +} + +func (r *router) Route(payload tss.Payload) { + switch payload.RpcSubmitTxResponse.Status { + case tss.TryAgainLaterStatus: + r.ErrorHandlerService.ProcessPayload(payload) + case tss.ErrorStatus: + if payload.RpcSubmitTxResponse.Code.OtherCodes == tss.NoCode { + if slices.Contains(RetryErrorCodes, payload.RpcSubmitTxResponse.Code.TxResultCode) { + r.ErrorHandlerService.ProcessPayload(payload) + } else if slices.Contains(FinalErrorCodes, payload.RpcSubmitTxResponse.Code.TxResultCode) { + r.WebhookHandlerService.ProcessPayload(payload) + } + } + default: + return + } +} diff --git a/internal/tss/router/router_test.go b/internal/tss/router/router_test.go new file mode 100644 index 0000000..8a5907a --- /dev/null +++ b/internal/tss/router/router_test.go @@ -0,0 +1,51 @@ +package router + +import ( + "testing" + + "github.com/stellar/go/xdr" + "github.com/stellar/wallet-backend/internal/tss" + "github.com/stellar/wallet-backend/internal/tss/services" +) + +func TestRouter(t *testing.T) { + errorHandlerService := services.MockService{} + defer errorHandlerService.AssertExpectations(t) + webhookHandlerService := services.MockService{} + router := NewRouter(RouterConfigs{ErrorHandlerService: &errorHandlerService, WebhookHandlerService: &webhookHandlerService}) + t.Run("status_try_again_later", func(t *testing.T) { + payload := tss.Payload{} + payload.RpcSubmitTxResponse.Status = tss.TryAgainLaterStatus + + errorHandlerService. + On("ProcessPayload", payload). + Return(). + Once() + + router.Route(payload) + }) + t.Run("error_status_route_to_error_handler_service", func(t *testing.T) { + payload := tss.Payload{} + payload.RpcSubmitTxResponse.Status = tss.ErrorStatus + payload.RpcSubmitTxResponse.Code.TxResultCode = xdr.TransactionResultCodeTxInsufficientFee + + errorHandlerService. + On("ProcessPayload", payload). + Return(). + Once() + + router.Route(payload) + }) + t.Run("error_status_route_to_webhook_handler_service", func(t *testing.T) { + payload := tss.Payload{} + payload.RpcSubmitTxResponse.Status = tss.ErrorStatus + payload.RpcSubmitTxResponse.Code.TxResultCode = xdr.TransactionResultCodeTxInsufficientBalance + + webhookHandlerService. + On("ProcessPayload", payload). + Return(). + Once() + + router.Route(payload) + }) +} diff --git a/internal/tss/services/error_handler_service.go b/internal/tss/services/error_handler_service.go new file mode 100644 index 0000000..2b50cd4 --- /dev/null +++ b/internal/tss/services/error_handler_service.go @@ -0,0 +1,39 @@ +package services + +import ( + "fmt" + "slices" + + "github.com/stellar/wallet-backend/internal/tss" +) + +type errorHandlerService struct { + JitterChannel tss.Channel + NonJitterChannel tss.Channel +} + +type ErrorHandlerServiceConfigs struct { + JitterChannel tss.Channel + NonJitterChannel tss.Channel +} + +func NewErrorHandlerService(cfg ErrorHandlerServiceConfigs) *errorHandlerService { + return &errorHandlerService{ + JitterChannel: cfg.JitterChannel, + NonJitterChannel: cfg.NonJitterChannel, + } +} + +func (p *errorHandlerService) ProcessPayload(payload tss.Payload) { + if payload.RpcSubmitTxResponse.Status == tss.TryAgainLaterStatus { + fmt.Println("TRY AGAIN LATER") + fmt.Println(payload) + p.JitterChannel.Send(payload) + } else { + if slices.Contains(tss.NonJitterErrorCodes, payload.RpcSubmitTxResponse.Code.TxResultCode) { + p.NonJitterChannel.Send(payload) + } else if slices.Contains(tss.JitterErrorCodes, payload.RpcSubmitTxResponse.Code.TxResultCode) { + p.JitterChannel.Send(payload) + } + } +} diff --git a/internal/tss/services/error_handler_service_test.go b/internal/tss/services/error_handler_service_test.go new file mode 100644 index 0000000..802cc8c --- /dev/null +++ b/internal/tss/services/error_handler_service_test.go @@ -0,0 +1,54 @@ +package services + +import ( + "testing" + + "github.com/stellar/go/xdr" + "github.com/stellar/wallet-backend/internal/tss" +) + +func TestProcessPayload(t *testing.T) { + jitterChannel := tss.MockChannel{} + defer jitterChannel.AssertExpectations(t) + nonJitterChannel := tss.MockChannel{} + defer nonJitterChannel.AssertExpectations(t) + + service := NewErrorHandlerService(ErrorHandlerServiceConfigs{JitterChannel: &jitterChannel, NonJitterChannel: &nonJitterChannel}) + + t.Run("status_try_again_later", func(t *testing.T) { + payload := tss.Payload{} + payload.RpcSubmitTxResponse.Status = tss.TryAgainLaterStatus + + jitterChannel. + On("Send", payload). + Return(). + Once() + + service.ProcessPayload(payload) + }) + t.Run("code_tx_too_early", func(t *testing.T) { + payload := tss.Payload{} + payload.RpcSubmitTxResponse.Status = tss.ErrorStatus + payload.RpcSubmitTxResponse.Code.TxResultCode = xdr.TransactionResultCodeTxTooEarly + + nonJitterChannel. + On("Send", payload). + Return(). + Once() + + service.ProcessPayload(payload) + }) + + t.Run("code_tx_insufficient_fee", func(t *testing.T) { + payload := tss.Payload{} + payload.RpcSubmitTxResponse.Status = tss.ErrorStatus + payload.RpcSubmitTxResponse.Code.TxResultCode = xdr.TransactionResultCodeTxInsufficientFee + + jitterChannel. + On("Send", payload). + Return(). + Once() + + service.ProcessPayload(payload) + }) +} diff --git a/internal/tss/services/mocks.go b/internal/tss/services/mocks.go new file mode 100644 index 0000000..fff8db8 --- /dev/null +++ b/internal/tss/services/mocks.go @@ -0,0 +1,16 @@ +package services + +import ( + "github.com/stellar/wallet-backend/internal/tss" + "github.com/stretchr/testify/mock" +) + +type MockService struct { + mock.Mock +} + +var _ Service = (*MockService)(nil) + +func (s *MockService) ProcessPayload(payload tss.Payload) { + s.Called(payload) +} diff --git a/internal/tss/services/types.go b/internal/tss/services/types.go new file mode 100644 index 0000000..f395190 --- /dev/null +++ b/internal/tss/services/types.go @@ -0,0 +1,7 @@ +package services + +import "github.com/stellar/wallet-backend/internal/tss" + +type Service interface { + ProcessPayload(payload tss.Payload) +} diff --git a/internal/tss/services/webhook_handler_service.go b/internal/tss/services/webhook_handler_service.go new file mode 100644 index 0000000..33c837e --- /dev/null +++ b/internal/tss/services/webhook_handler_service.go @@ -0,0 +1,19 @@ +package services + +import ( + "github.com/stellar/wallet-backend/internal/tss" +) + +type webhookHandlerService struct { + channel tss.Channel +} + +func NewWebhookHandlerService(channel tss.Channel) Service { + return &webhookHandlerService{ + channel: channel, + } +} + +func (p *webhookHandlerService) ProcessPayload(payload tss.Payload) { + // fill in later +} diff --git a/internal/tss/store/store.go b/internal/tss/store/store.go new file mode 100644 index 0000000..5dcc97f --- /dev/null +++ b/internal/tss/store/store.go @@ -0,0 +1,73 @@ +package store + +import ( + "context" + "fmt" + + "github.com/stellar/wallet-backend/internal/db" + "github.com/stellar/wallet-backend/internal/tss" +) + +type Store interface { + UpsertTransaction(ctx context.Context, WebhookURL string, txHash string, txXDR string, status tss.RPCTXStatus) error + UpsertTry(ctx context.Context, transactionHash string, feeBumpTxHash string, feeBumpTxXDR string, status tss.RPCTXCode) error +} + +var _ Store = (*store)(nil) + +type store struct { + DB db.ConnectionPool +} + +func NewStore(db db.ConnectionPool) Store { + return &store{ + DB: db, + } +} + +func (s *store) UpsertTransaction(ctx context.Context, webhookURL string, txHash string, txXDR string, status tss.RPCTXStatus) error { + const q = ` + INSERT INTO + tss_transactions (transaction_hash, transaction_xdr, webhook_url, current_status) + VALUES + ($1, $2, $3, $4) + ON CONFLICT (transaction_hash) + DO UPDATE SET + transaction_xdr = $2, + webhook_url = $3, + current_status = $4, + updated_at = NOW(); + ` + _, err := s.DB.ExecContext(ctx, q, txHash, txXDR, webhookURL, string(status)) + if err != nil { + return fmt.Errorf("inserting/updatig tss transaction: %w", err) + } + return nil +} + +func (s *store) UpsertTry(ctx context.Context, txHash string, feeBumpTxHash string, feeBumpTxXDR string, status tss.RPCTXCode) error { + const q = ` + INSERT INTO + tss_transaction_submission_tries (original_transaction_hash, try_transaction_hash, try_transaction_xdr, status) + VALUES + ($1, $2, $3, $4) + ON CONFLICT (try_transaction_hash) + DO UPDATE SET + original_transaction_hash = $1, + try_transaction_xdr = $3, + status = $4, + updated_at = NOW(); + ` + var st int + // if this value is set, it takes precedence over the code from RPC + if status.OtherCodes != tss.NoCode { + st = int(status.OtherCodes) + } else { + st = int(status.TxResultCode) + } + _, err := s.DB.ExecContext(ctx, q, txHash, feeBumpTxHash, feeBumpTxXDR, st) + if err != nil { + return fmt.Errorf("inserting/updating tss try: %w", err) + } + return nil +} diff --git a/internal/tss/store/store_test.go b/internal/tss/store/store_test.go new file mode 100644 index 0000000..57709ab --- /dev/null +++ b/internal/tss/store/store_test.go @@ -0,0 +1,96 @@ +package store + +import ( + "context" + "testing" + + "github.com/stellar/go/xdr" + "github.com/stellar/wallet-backend/internal/db" + "github.com/stellar/wallet-backend/internal/db/dbtest" + "github.com/stellar/wallet-backend/internal/tss" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestUpsertTransaction(t *testing.T) { + dbt := dbtest.Open(t) + defer dbt.Close() + dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) + require.NoError(t, err) + defer dbConnectionPool.Close() + store := NewStore(dbConnectionPool) + t.Run("insert", func(t *testing.T) { + _ = store.UpsertTransaction(context.Background(), "www.stellar.org", "hash", "xdr", tss.NewStatus) + + var status string + err = dbConnectionPool.GetContext(context.Background(), &status, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, "hash") + require.NoError(t, err) + assert.Equal(t, status, string(tss.NewStatus)) + }) + + t.Run("update", func(t *testing.T) { + _ = store.UpsertTransaction(context.Background(), "www.stellar.org", "hash", "xdr", tss.NewStatus) + _ = store.UpsertTransaction(context.Background(), "www.stellar.org", "hash", "xdr", tss.SuccessStatus) + + var status string + err = dbConnectionPool.GetContext(context.Background(), &status, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, "hash") + require.NoError(t, err) + assert.Equal(t, status, string(tss.SuccessStatus)) + + var numRows int + err = dbConnectionPool.GetContext(context.Background(), &numRows, `SELECT count(*) FROM tss_transactions WHERE transaction_hash = $1`, "hash") + require.NoError(t, err) + assert.Equal(t, numRows, 1) + + }) +} + +func TestUpsertTry(t *testing.T) { + dbt := dbtest.Open(t) + defer dbt.Close() + dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) + require.NoError(t, err) + defer dbConnectionPool.Close() + store := NewStore(dbConnectionPool) + t.Run("insert", func(t *testing.T) { + code := tss.RPCTXCode{OtherCodes: tss.NewCode} + _ = store.UpsertTry(context.Background(), "hash", "feebumptxhash", "feebumptxxdr", code) + + var status int + err = dbConnectionPool.GetContext(context.Background(), &status, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, "feebumptxhash") + require.NoError(t, err) + assert.Equal(t, status, int(tss.NewCode)) + }) + + t.Run("update_other_code", func(t *testing.T) { + code := tss.RPCTXCode{OtherCodes: tss.NewCode} + _ = store.UpsertTry(context.Background(), "hash", "feebumptxhash", "feebumptxxdr", code) + code = tss.RPCTXCode{OtherCodes: tss.RPCFailCode} + _ = store.UpsertTry(context.Background(), "hash", "feebumptxhash", "feebumptxxdr", code) + var status int + err = dbConnectionPool.GetContext(context.Background(), &status, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, "feebumptxhash") + require.NoError(t, err) + assert.Equal(t, status, int(tss.RPCFailCode)) + + var numRows int + err = dbConnectionPool.GetContext(context.Background(), &numRows, `SELECT count(*) FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, "feebumptxhash") + require.NoError(t, err) + assert.Equal(t, numRows, 1) + }) + + t.Run("update_tx_code", func(t *testing.T) { + code := tss.RPCTXCode{OtherCodes: tss.NewCode} + _ = store.UpsertTry(context.Background(), "hash", "feebumptxhash", "feebumptxxdr", code) + code = tss.RPCTXCode{TxResultCode: xdr.TransactionResultCodeTxSuccess} + _ = store.UpsertTry(context.Background(), "hash", "feebumptxhash", "feebumptxxdr", code) + var status int + err = dbConnectionPool.GetContext(context.Background(), &status, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, "feebumptxhash") + require.NoError(t, err) + assert.Equal(t, status, int(xdr.TransactionResultCodeTxSuccess)) + + var numRows int + err = dbConnectionPool.GetContext(context.Background(), &numRows, `SELECT count(*) FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, "feebumptxhash") + require.NoError(t, err) + assert.Equal(t, numRows, 1) + }) +} diff --git a/internal/tss/types.go b/internal/tss/types.go index 3f055f4..85202d9 100644 --- a/internal/tss/types.go +++ b/internal/tss/types.go @@ -35,6 +35,17 @@ const ( SuccessStatus RPCTXStatus = "SUCCESS" ) +var NonJitterErrorCodes = []xdr.TransactionResultCode{ + xdr.TransactionResultCodeTxTooEarly, + xdr.TransactionResultCodeTxTooLate, + xdr.TransactionResultCodeTxBadSeq, +} + +var JitterErrorCodes = []xdr.TransactionResultCode{ + xdr.TransactionResultCodeTxInsufficientFee, + xdr.TransactionResultCodeTxInternalError, +} + type RPCGetIngestTxResponse struct { // A status that indicated whether this transaction failed or successly made it to the ledger Status RPCTXStatus diff --git a/internal/tss/utils/helpers.go b/internal/tss/utils/helpers.go new file mode 100644 index 0000000..c844a03 --- /dev/null +++ b/internal/tss/utils/helpers.go @@ -0,0 +1,39 @@ +package utils + +import ( + "github.com/stellar/go/keypair" + "github.com/stellar/go/txnbuild" +) + +func BuildTestTransaction() *txnbuild.Transaction { + accountToSponsor := keypair.MustRandom() + + tx, _ := txnbuild.NewTransaction(txnbuild.TransactionParams{ + SourceAccount: &txnbuild.SimpleAccount{ + AccountID: accountToSponsor.Address(), + Sequence: 124, + }, + IncrementSequenceNum: true, + Operations: []txnbuild.Operation{ + &txnbuild.Payment{ + Destination: keypair.MustRandom().Address(), + Amount: "14", + Asset: txnbuild.NativeAsset{}, + }, + }, + BaseFee: 104, + Preconditions: txnbuild.Preconditions{TimeBounds: txnbuild.NewTimeout(10)}, + }) + return tx +} + +func BuildTestFeeBumpTransaction() *txnbuild.FeeBumpTransaction { + + feeBumpTx, _ := txnbuild.NewFeeBumpTransaction( + txnbuild.FeeBumpTransactionParams{ + Inner: BuildTestTransaction(), + FeeAccount: keypair.MustRandom().Address(), + BaseFee: 110, + }) + return feeBumpTx +} diff --git a/internal/tss/utils/mocks.go b/internal/tss/utils/mocks.go new file mode 100644 index 0000000..597a6c9 --- /dev/null +++ b/internal/tss/utils/mocks.go @@ -0,0 +1,50 @@ +package utils + +import ( + "context" + "io" + "net/http" + + "github.com/stellar/go/txnbuild" + "github.com/stellar/wallet-backend/internal/tss" + "github.com/stretchr/testify/mock" +) + +type MockHTTPClient struct { + mock.Mock +} + +func (s *MockHTTPClient) Post(url, contentType string, body io.Reader) (resp *http.Response, err error) { + args := s.Called(url, contentType, body) + return args.Get(0).(*http.Response), args.Error(1) +} + +type TransactionServiceMock struct { + mock.Mock +} + +var _ TransactionService = (*TransactionServiceMock)(nil) + +func (t *TransactionServiceMock) NetworkPassphrase() string { + args := t.Called() + return args.String(0) +} + +func (t *TransactionServiceMock) SignAndBuildNewFeeBumpTransaction(ctx context.Context, origTxXdr string) (*txnbuild.FeeBumpTransaction, error) { + args := t.Called(ctx, origTxXdr) + if result := args.Get(0); result != nil { + return result.(*txnbuild.FeeBumpTransaction), args.Error(1) + } + return nil, args.Error(1) + +} + +func (t *TransactionServiceMock) SendTransaction(transactionXdr string) (tss.RPCSendTxResponse, error) { + args := t.Called(transactionXdr) + return args.Get(0).(tss.RPCSendTxResponse), args.Error(1) +} + +func (t *TransactionServiceMock) GetTransaction(transactionHash string) (tss.RPCGetIngestTxResponse, error) { + args := t.Called(transactionHash) + return args.Get(0).(tss.RPCGetIngestTxResponse), args.Error(1) +} diff --git a/internal/tss/utils/transaction_service.go b/internal/tss/utils/transaction_service.go index e38b885..35f89a0 100644 --- a/internal/tss/utils/transaction_service.go +++ b/internal/tss/utils/transaction_service.go @@ -3,44 +3,53 @@ package utils import ( "bytes" "context" + "encoding/base64" "encoding/json" "fmt" "io" "net/http" "strconv" + "strings" + xdr3 "github.com/stellar/go-xdr/xdr3" "github.com/stellar/go/clients/horizonclient" "github.com/stellar/go/txnbuild" "github.com/stellar/go/xdr" "github.com/stellar/wallet-backend/internal/signing" "github.com/stellar/wallet-backend/internal/tss" - tssErr "github.com/stellar/wallet-backend/internal/tss/errors" + tsserror "github.com/stellar/wallet-backend/internal/tss/errors" ) -var ( - RpcPost = http.Post - UnMarshalRPCResponse = io.ReadAll - UnMarshalJSON = parseJSONBody - callRPC = sendRPCRequest - UnMarshalErrorResultXdr = parseErrorResultXdr -) +type HTTPClient interface { + Post(url string, t string, body io.Reader) (resp *http.Response, err error) +} + +type TransactionService interface { + NetworkPassphrase() string + SignAndBuildNewFeeBumpTransaction(ctx context.Context, origTxXdr string) (*txnbuild.FeeBumpTransaction, error) + SendTransaction(transactionXdr string) (tss.RPCSendTxResponse, error) + GetTransaction(transactionHash string) (tss.RPCGetIngestTxResponse, error) +} type transactionService struct { DistributionAccountSignatureClient signing.SignatureClient ChannelAccountSignatureClient signing.SignatureClient HorizonClient horizonclient.ClientInterface - RpcUrl string + RPCURL string BaseFee int64 + HTTPClient HTTPClient Ctx context.Context } +var _ TransactionService = (*transactionService)(nil) + type TransactionServiceOptions struct { DistributionAccountSignatureClient signing.SignatureClient ChannelAccountSignatureClient signing.SignatureClient HorizonClient horizonclient.ClientInterface - RpcUrl string + RPCURL string BaseFee int64 - Ctx context.Context + HTTPClient HTTPClient } func (o *TransactionServiceOptions) ValidateOptions() error { @@ -56,17 +65,22 @@ func (o *TransactionServiceOptions) ValidateOptions() error { return fmt.Errorf("horizon client cannot be nil") } - if o.RpcUrl == "" { + if o.RPCURL == "" { return fmt.Errorf("rpc url cannot be empty") } if o.BaseFee < int64(txnbuild.MinBaseFee) { return fmt.Errorf("base fee is lower than the minimum network fee") } + + if o.HTTPClient == nil { + return fmt.Errorf("http client cannot be nil") + } + return nil } -func NewTransactionService(opts TransactionServiceOptions) (TransactionService, error) { +func NewTransactionService(opts TransactionServiceOptions) (*transactionService, error) { if err := opts.ValidateOptions(); err != nil { return nil, err } @@ -74,73 +88,57 @@ func NewTransactionService(opts TransactionServiceOptions) (TransactionService, DistributionAccountSignatureClient: opts.DistributionAccountSignatureClient, ChannelAccountSignatureClient: opts.ChannelAccountSignatureClient, HorizonClient: opts.HorizonClient, - RpcUrl: opts.RpcUrl, + RPCURL: opts.RPCURL, BaseFee: opts.BaseFee, - Ctx: opts.Ctx, + HTTPClient: opts.HTTPClient, }, nil } -func parseJSONBody(body []byte) (map[string]interface{}, error) { - var res map[string]interface{} - err := json.Unmarshal(body, &res) - if err != nil { - return nil, fmt.Errorf(err.Error()) - } - return res, nil -} - -func parseErrorResultXdr(errorResultXdr string) (tss.RPCTXCode, error) { - errorResult := xdr.TransactionResult{} - err := errorResult.UnmarshalBinary([]byte(errorResultXdr)) - - if err != nil { - return tss.RPCTXCode{OtherCodes: tss.UnMarshalBinaryCode}, fmt.Errorf("SendTransaction: unable to unmarshal errorResultXdr: %s", errorResultXdr) - } - return tss.RPCTXCode{ - TxResultCode: errorResult.Result.Code, - }, nil -} - -func sendRPCRequest(rpcUrl string, method string, params map[string]string) (map[string]interface{}, error) { +func (t *transactionService) sendRPCRequest(method string, params map[string]string) (map[string]interface{}, error) { payload := map[string]interface{}{ "jsonrpc": "2.0", "id": 1, "method": method, "params": params, } - jsonData, _ := json.Marshal(payload) + jsonData, err := json.Marshal(payload) + + if err != nil { + return nil, fmt.Errorf("marshaling payload") + } - resp, err := RpcPost(rpcUrl, "application/json", bytes.NewBuffer(jsonData)) + resp, err := t.HTTPClient.Post(t.RPCURL, "application/json", bytes.NewBuffer(jsonData)) if err != nil { - return nil, fmt.Errorf(method+": sending POST request to rpc: %v", err) + return nil, fmt.Errorf("%s: sending POST request to rpc: %v", method, err) } defer resp.Body.Close() - body, err := UnMarshalRPCResponse(resp.Body) + body, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf(method+": unmarshalling rpc response: %v", err) + return nil, fmt.Errorf("%s: unmarshaling RPC response", method) } - res, err := UnMarshalJSON(body) + var res map[string]interface{} + err = json.Unmarshal(body, &res) if err != nil { - return nil, fmt.Errorf(method+": parsing rpc response JSON: %v", err) + return nil, fmt.Errorf("%s: parsing RPC response JSON", method) } return res, nil } -func (t *transactionService) NetworkPassPhrase() string { +func (t *transactionService) NetworkPassphrase() string { return t.DistributionAccountSignatureClient.NetworkPassphrase() } -func (t *transactionService) SignAndBuildNewTransaction(origTxXdr string) (*txnbuild.FeeBumpTransaction, error) { +func (t *transactionService) SignAndBuildNewFeeBumpTransaction(ctx context.Context, origTxXdr string) (*txnbuild.FeeBumpTransaction, error) { genericTx, err := txnbuild.TransactionFromXDR(origTxXdr) if err != nil { - return nil, tssErr.OriginalXdrMalformed + return nil, tsserror.OriginalXDRMalformed } originalTx, txEmpty := genericTx.Transaction() if !txEmpty { - return nil, tssErr.OriginalXdrMalformed + return nil, tsserror.OriginalXDRMalformed } - channelAccountPublicKey, err := t.ChannelAccountSignatureClient.GetAccountPublicKey(t.Ctx) + channelAccountPublicKey, err := t.ChannelAccountSignatureClient.GetAccountPublicKey(ctx) if err != nil { return nil, fmt.Errorf("getting channel account public key: %w", err) } @@ -162,12 +160,12 @@ func (t *transactionService) SignAndBuildNewTransaction(origTxXdr string) (*txnb if err != nil { return nil, fmt.Errorf("building transaction: %w", err) } - tx, err = t.ChannelAccountSignatureClient.SignStellarTransaction(t.Ctx, tx, channelAccountPublicKey) + tx, err = t.ChannelAccountSignatureClient.SignStellarTransaction(ctx, tx, channelAccountPublicKey) if err != nil { return nil, fmt.Errorf("signing transaction with channel account: %w", err) } - // wrap the transaction in a fee bump tx, signed by the distribution account - distributionAccountPublicKey, err := t.DistributionAccountSignatureClient.GetAccountPublicKey(t.Ctx) + // Wrap the transaction in a fee bump tx, signed by the distribution account + distributionAccountPublicKey, err := t.DistributionAccountSignatureClient.GetAccountPublicKey(ctx) if err != nil { return nil, fmt.Errorf("getting distribution account public key: %w", err) } @@ -183,15 +181,37 @@ func (t *transactionService) SignAndBuildNewTransaction(origTxXdr string) (*txnb return nil, fmt.Errorf("building fee-bump transaction %w", err) } - feeBumpTx, err = t.DistributionAccountSignatureClient.SignStellarFeeBumpTransaction(t.Ctx, feeBumpTx) + feeBumpTx, err = t.DistributionAccountSignatureClient.SignStellarFeeBumpTransaction(ctx, feeBumpTx) if err != nil { return nil, fmt.Errorf("signing the fee bump transaction with distribution account: %w", err) } return feeBumpTx, nil } +func (t *transactionService) parseErrorResultXDR(errorResultXdr string) (tss.RPCTXCode, error) { + + //errorResult := xdr.TransactionResult{} + unMarshallErr := "unable to unmarshal errorResultXdr: %s" + //err := errorResult.UnmarshalBinary([]byte(errorResultXdr)) + + decodedBytes, err := base64.StdEncoding.DecodeString(errorResultXdr) + if err != nil { + return tss.RPCTXCode{OtherCodes: tss.UnMarshalBinaryCode}, fmt.Errorf(unMarshallErr, errorResultXdr) + } + dec := xdr3.NewDecoder(strings.NewReader(string(decodedBytes))) + var errorResult xdr.TransactionResult + _, err = dec.Decode(&errorResult) + + if err != nil { + return tss.RPCTXCode{OtherCodes: tss.UnMarshalBinaryCode}, fmt.Errorf(unMarshallErr, errorResultXdr) + } + return tss.RPCTXCode{ + TxResultCode: errorResult.Result.Code, + }, nil +} + func (t *transactionService) SendTransaction(transactionXdr string) (tss.RPCSendTxResponse, error) { - rpcResponse, err := callRPC(t.RpcUrl, "sendTransaction", map[string]string{"transaction": transactionXdr}) + rpcResponse, err := t.sendRPCRequest("sendTransaction", map[string]string{"transaction": transactionXdr}) sendTxResponse := tss.RPCSendTxResponse{} sendTxResponse.TransactionXDR = transactionXdr if err != nil { @@ -200,41 +220,52 @@ func (t *transactionService) SendTransaction(transactionXdr string) (tss.RPCSend } if result, ok := rpcResponse["result"].(map[string]interface{}); ok { - if val, exists := result["status"].(tss.RPCTXStatus); exists { - sendTxResponse.Status = val + if val, exists := result["status"].(string); exists { + sendTxResponse.Status = tss.RPCTXStatus(val) } if val, exists := result["errorResultXdr"].(string); exists { - sendTxResponse.Code, err = UnMarshalErrorResultXdr(val) + sendTxResponse.Code, err = t.parseErrorResultXDR(val) } if hash, exists := result["hash"].(string); exists { sendTxResponse.TransactionHash = hash } + } else { + sendTxResponse.Code.OtherCodes = tss.RPCFailCode + return sendTxResponse, fmt.Errorf("RPC response has no result field") } return sendTxResponse, err } func (t *transactionService) GetTransaction(transactionHash string) (tss.RPCGetIngestTxResponse, error) { - rpcResponse, err := callRPC(t.RpcUrl, "getTransaction", map[string]string{"hash": transactionHash}) + rpcResponse, err := t.sendRPCRequest("getTransaction", map[string]string{"hash": transactionHash}) if err != nil { - return tss.RPCGetIngestTxResponse{}, fmt.Errorf(err.Error()) + return tss.RPCGetIngestTxResponse{Status: tss.ErrorStatus}, fmt.Errorf(err.Error()) } getIngestTxResponse := tss.RPCGetIngestTxResponse{} if result, ok := rpcResponse["result"].(map[string]interface{}); ok { - if status, exists := result["status"].(tss.RPCTXStatus); exists { - getIngestTxResponse.Status = status + if status, exists := result["status"].(string); exists { + getIngestTxResponse.Status = tss.RPCTXStatus(status) } - if envelopeXdr, exists := result["envelopeXdr"].(string); exists { - getIngestTxResponse.EnvelopeXDR = envelopeXdr + if envelopeXDR, exists := result["envelopeXdr"].(string); exists { + getIngestTxResponse.EnvelopeXDR = envelopeXDR } - if resultXdr, exists := result["resultXdr"].(string); exists { - getIngestTxResponse.ResultXDR = resultXdr + if resultXDR, exists := result["resultXdr"].(string); exists { + getIngestTxResponse.ResultXDR = resultXDR } if createdAt, exists := result["createdAt"].(string); exists { - // we can supress erroneous createdAt errors as this is not an important field - createdAtInt, _ := strconv.ParseInt(createdAt, 10, 64) - getIngestTxResponse.CreatedAt = createdAtInt + createdAtInt, e := strconv.ParseInt(createdAt, 10, 64) + if e != nil { + getIngestTxResponse.Status = tss.ErrorStatus + err = fmt.Errorf("cannot parse createdAt") + } else { + getIngestTxResponse.CreatedAt = createdAtInt + } } + } else { + getIngestTxResponse.Status = tss.ErrorStatus + return getIngestTxResponse, fmt.Errorf("RPC response has no result field") + } - return getIngestTxResponse, nil + return getIngestTxResponse, err } diff --git a/internal/tss/utils/transaction_service_test.go b/internal/tss/utils/transaction_service_test.go index 174f2d6..f58d2fd 100644 --- a/internal/tss/utils/transaction_service_test.go +++ b/internal/tss/utils/transaction_service_test.go @@ -5,8 +5,10 @@ import ( "context" "encoding/json" "errors" + "fmt" "io" "net/http" + "strings" "testing" "github.com/stellar/go/clients/horizonclient" @@ -16,7 +18,7 @@ import ( "github.com/stellar/go/xdr" "github.com/stellar/wallet-backend/internal/signing" "github.com/stellar/wallet-backend/internal/tss" - tssErr "github.com/stellar/wallet-backend/internal/tss/errors" + tsserror "github.com/stellar/wallet-backend/internal/tss/errors" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) @@ -44,38 +46,41 @@ func buildTestTransaction() *txnbuild.Transaction { } func TestValidateOptions(t *testing.T) { - t.Run("return_error_when_distribution_signature_client_null", func(t *testing.T) { + t.Run("return_error_when_distribution_signature_client_nil", func(t *testing.T) { opts := TransactionServiceOptions{ DistributionAccountSignatureClient: nil, ChannelAccountSignatureClient: &signing.SignatureClientMock{}, HorizonClient: &horizonclient.MockClient{}, - RpcUrl: "http://localhost:8000/soroban/rpc", + RPCURL: "http://localhost:8000/soroban/rpc", BaseFee: 114, + HTTPClient: &MockHTTPClient{}, } err := opts.ValidateOptions() assert.Equal(t, "distribution account signature client cannot be nil", err.Error()) }) - t.Run("return_error_when_channel_signature_client_null", func(t *testing.T) { + t.Run("return_error_when_channel_signature_client_nil", func(t *testing.T) { opts := TransactionServiceOptions{ DistributionAccountSignatureClient: &signing.SignatureClientMock{}, ChannelAccountSignatureClient: nil, HorizonClient: &horizonclient.MockClient{}, - RpcUrl: "http://localhost:8000/soroban/rpc", + RPCURL: "http://localhost:8000/soroban/rpc", BaseFee: 114, + HTTPClient: &MockHTTPClient{}, } err := opts.ValidateOptions() assert.Equal(t, "channel account signature client cannot be nil", err.Error()) }) - t.Run("return_error_when_horizon_client_null", func(t *testing.T) { + t.Run("return_error_when_horizon_client_nil", func(t *testing.T) { opts := TransactionServiceOptions{ DistributionAccountSignatureClient: &signing.SignatureClientMock{}, ChannelAccountSignatureClient: &signing.SignatureClientMock{}, HorizonClient: nil, - RpcUrl: "http://localhost:8000/soroban/rpc", + RPCURL: "http://localhost:8000/soroban/rpc", BaseFee: 114, + HTTPClient: &MockHTTPClient{}, } err := opts.ValidateOptions() assert.Equal(t, "horizon client cannot be nil", err.Error()) @@ -86,8 +91,9 @@ func TestValidateOptions(t *testing.T) { DistributionAccountSignatureClient: &signing.SignatureClientMock{}, ChannelAccountSignatureClient: &signing.SignatureClientMock{}, HorizonClient: &horizonclient.MockClient{}, - RpcUrl: "", + RPCURL: "", BaseFee: 114, + HTTPClient: &MockHTTPClient{}, } err := opts.ValidateOptions() assert.Equal(t, "rpc url cannot be empty", err.Error()) @@ -98,15 +104,28 @@ func TestValidateOptions(t *testing.T) { DistributionAccountSignatureClient: &signing.SignatureClientMock{}, ChannelAccountSignatureClient: &signing.SignatureClientMock{}, HorizonClient: &horizonclient.MockClient{}, - RpcUrl: "http://localhost:8000/soroban/rpc", + RPCURL: "http://localhost:8000/soroban/rpc", BaseFee: txnbuild.MinBaseFee - 10, + HTTPClient: &MockHTTPClient{}, } err := opts.ValidateOptions() assert.Equal(t, "base fee is lower than the minimum network fee", err.Error()) }) + + t.Run("return_error_http_client_nil", func(t *testing.T) { + opts := TransactionServiceOptions{ + DistributionAccountSignatureClient: &signing.SignatureClientMock{}, + ChannelAccountSignatureClient: &signing.SignatureClientMock{}, + HorizonClient: &horizonclient.MockClient{}, + RPCURL: "http://localhost:8000/soroban/rpc", + BaseFee: 114, + } + err := opts.ValidateOptions() + assert.Equal(t, "http client cannot be nil", err.Error()) + }) } -func TestSignAndBuildNewTransaction(t *testing.T) { +func TestSignAndBuildNewFeeBumpTransaction(t *testing.T) { distributionAccountSignatureClient := signing.SignatureClientMock{} defer distributionAccountSignatureClient.AssertExpectations(t) channelAccountSignatureClient := signing.SignatureClientMock{} @@ -117,17 +136,17 @@ func TestSignAndBuildNewTransaction(t *testing.T) { DistributionAccountSignatureClient: &distributionAccountSignatureClient, ChannelAccountSignatureClient: &channelAccountSignatureClient, HorizonClient: &horizonClient, - RpcUrl: "http://localhost:8000/soroban/rpc", + RPCURL: "http://localhost:8000/soroban/rpc", BaseFee: 114, - Ctx: context.Background(), + HTTPClient: &MockHTTPClient{}, }) txStr, _ := buildTestTransaction().Base64() t.Run("malformed_transaction_string", func(t *testing.T) { - feeBumpTx, err := txService.SignAndBuildNewTransaction("abcd") + feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), "abcd") assert.Empty(t, feeBumpTx) - assert.ErrorIs(t, tssErr.OriginalXdrMalformed, err) + assert.ErrorIs(t, tsserror.OriginalXDRMalformed, err) }) t.Run("channel_account_signature_client_get_account_public_key_err", func(t *testing.T) { @@ -136,7 +155,7 @@ func TestSignAndBuildNewTransaction(t *testing.T) { Return("", errors.New("channel accounts unavailable")). Once() - feeBumpTx, err := txService.SignAndBuildNewTransaction(txStr) + feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) assert.Empty(t, feeBumpTx) assert.Equal(t, "getting channel account public key: channel accounts unavailable", err.Error()) }) @@ -155,7 +174,7 @@ func TestSignAndBuildNewTransaction(t *testing.T) { Return(horizon.Account{}, errors.New("horizon down")). Once() - feeBumpTx, err := txService.SignAndBuildNewTransaction(txStr) + feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) assert.Empty(t, feeBumpTx) assert.Equal(t, "getting channel account details from horizon: horizon down", err.Error()) }) @@ -177,7 +196,7 @@ func TestSignAndBuildNewTransaction(t *testing.T) { Return(horizon.Account{AccountID: channelAccount.Address(), Sequence: 1}, nil). Once() - feeBumpTx, err := txService.SignAndBuildNewTransaction(txStr) + feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) assert.Empty(t, feeBumpTx) assert.Equal(t, "signing transaction with channel account: unable to sign", err.Error()) }) @@ -205,7 +224,7 @@ func TestSignAndBuildNewTransaction(t *testing.T) { Return(horizon.Account{AccountID: channelAccount.Address(), Sequence: 1}, nil). Once() - feeBumpTx, err := txService.SignAndBuildNewTransaction(txStr) + feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) assert.Empty(t, feeBumpTx) assert.Equal(t, "getting distribution account public key: client down", err.Error()) }) @@ -236,7 +255,7 @@ func TestSignAndBuildNewTransaction(t *testing.T) { Return(horizon.Account{AccountID: account.Address(), Sequence: 1}, nil). Once() - feeBumpTx, err := txService.SignAndBuildNewTransaction(txStr) + feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) assert.Empty(t, feeBumpTx) assert.Equal(t, "signing the fee bump transaction with distribution account: unable to sign", err.Error()) }) @@ -274,321 +293,346 @@ func TestSignAndBuildNewTransaction(t *testing.T) { Return(horizon.Account{AccountID: account.Address(), Sequence: 1}, nil). Once() - feeBumpTx, err := txService.SignAndBuildNewTransaction(txStr) + feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) assert.Equal(t, feeBumpTx, testFeeBumpTx) assert.Empty(t, err) }) } -type MockPost struct { - mock.Mock -} - -func (m *MockPost) Post(url string, content string, body io.Reader) (*http.Response, error) { - args := m.Called(url, content, body) - return args.Get(0).(*http.Response), args.Error(1) -} - -type MockUnMarshallRPCResponse struct { - mock.Mock -} - -func (m *MockUnMarshallRPCResponse) ReadAll(r io.Reader) ([]byte, error) { - args := m.Called(r) - return args.Get(0).(([]byte)), args.Error(1) - -} +type errorReader struct{} -type MockUnMarshalJSON struct { - mock.Mock +func (e *errorReader) Read(p []byte) (n int, err error) { + return 0, fmt.Errorf("read error") } -func (m *MockUnMarshalJSON) UnMarshalJSONBody(body []byte) (map[string]interface{}, error) { - args := m.Called(body) - if args.Get(0) == nil { - return nil, args.Error(1) - } - return args.Get(0).(map[string]interface{}), args.Error(1) +func (e *errorReader) Close() error { + return nil } -func TestCallRPC(t *testing.T) { - mockPost := MockPost{} - RpcPost = mockPost.Post - defer func() { RpcPost = http.Post }() - mockUnMarshalRPCResponse := MockUnMarshallRPCResponse{} - UnMarshalRPCResponse = mockUnMarshalRPCResponse.ReadAll - defer func() { UnMarshalRPCResponse = io.ReadAll }() - mockUnMarshalJSON := MockUnMarshalJSON{} - UnMarshalJSON = mockUnMarshalJSON.UnMarshalJSONBody - defer func() { UnMarshalJSON = parseJSONBody }() +func TestSendRPCRequest(t *testing.T) { + mockHTTPClient := MockHTTPClient{} + rpcURL := "http://localhost:8000/soroban/rpc" + txService, _ := NewTransactionService(TransactionServiceOptions{ + DistributionAccountSignatureClient: &signing.SignatureClientMock{}, + ChannelAccountSignatureClient: &signing.SignatureClientMock{}, + HorizonClient: &horizonclient.MockClient{}, + RPCURL: rpcURL, + BaseFee: 114, + HTTPClient: &mockHTTPClient, + }) + method := "sendTransaction" params := map[string]string{"transaction": "ABCD"} payload := map[string]interface{}{ "jsonrpc": "2.0", "id": 1, - "method": "sendTransaction", + "method": method, "params": params, } jsonData, _ := json.Marshal(payload) - rpcUrl := "http://localhost:8000/soroban/rpc" - t.Run("rpc_post_call_fails", func(t *testing.T) { - mockPost. - On("Post", rpcUrl, "application/json", bytes.NewBuffer(jsonData)). - Return(&http.Response{}, errors.New("connection error")). + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(&http.Response{}, errors.New("RPC Connection fail")). Once() - response, err := callRPC(rpcUrl, "sendTransaction", params) + resp, err := txService.sendRPCRequest(method, params) - assert.Empty(t, response) - assert.Equal(t, "sendTransaction: sending POST request to rpc: connection error", err.Error()) + assert.Empty(t, resp) + assert.Equal(t, "sendTransaction: sending POST request to rpc: RPC Connection fail", err.Error()) }) - t.Run("unmarshal_rpc_response_fails", func(t *testing.T) { - mockResponse := &http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(`{"mock": "response"}`)), - } - - mockPost. - On("Post", rpcUrl, "application/json", bytes.NewBuffer(jsonData)). - Return(mockResponse, nil). - Once() - mockUnMarshalRPCResponse. - On("ReadAll", mockResponse.Body). - Return([]byte{}, errors.New("bad string")). + t.Run("unmarshaling_rpc_response_fails", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(&errorReader{}), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). Once() - response, err := callRPC(rpcUrl, "sendTransaction", params) + resp, err := txService.sendRPCRequest(method, params) - assert.Empty(t, response) - assert.Equal(t, "sendTransaction: unmarshalling rpc response: bad string", err.Error()) + assert.Empty(t, resp) + assert.Equal(t, "sendTransaction: unmarshaling RPC response", err.Error()) }) - t.Run("unmarshal_json_fails", func(t *testing.T) { - mockResponse := &http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(`{"mock": "response"}`)), + t.Run("unmarshaling_json_fails", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{invalid-json`)), } - - mockPost. - On("Post", rpcUrl, "application/json", mock.AnythingOfType("*bytes.Buffer")). - Return(mockResponse, nil). + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). Once() - body := []byte("response") - mockUnMarshalRPCResponse. - On("ReadAll", mockResponse.Body). - Return(body, nil). - Once() + resp, err := txService.sendRPCRequest(method, params) - mockUnMarshalJSON. - On("UnMarshalJSONBody", body). - Return(nil, errors.New("bad json format")). - Once() - - response, err := callRPC(rpcUrl, "sendTransaction", params) - - assert.Empty(t, response) - assert.Equal(t, "sendTransaction: parsing rpc response JSON: bad json format", err.Error()) + assert.Empty(t, resp) + assert.Equal(t, "sendTransaction: parsing RPC response JSON", err.Error()) }) - t.Run("returns_unmarshalled_value", func(t *testing.T) { - mockResponse := &http.Response{ - StatusCode: 200, - Body: io.NopCloser(bytes.NewBufferString(`{"mock": "response"}`)), - } - mockPost. - On("Post", rpcUrl, "application/json", mock.AnythingOfType("*bytes.Buffer")). - Return(mockResponse, nil). - Once() - - body := []byte("response") - mockUnMarshalRPCResponse. - On("ReadAll", mockResponse.Body). - Return(body, nil). - Once() - - expectedResponse := map[string]interface{}{"jsonrpc": "2.0", "id": 1, "result": map[string]interface{}{"status": "SUCCESS", "envelopeXdr": "ABCD"}} - - mockUnMarshalJSON. - On("UnMarshalJSONBody", body). - Return(expectedResponse, nil). + t.Run("returns_rpc_response", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"status": "success"}`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). Once() - rpcResponse, err := callRPC(rpcUrl, "sendTransaction", params) + resp, err := txService.sendRPCRequest(method, params) - assert.Equal(t, rpcResponse, expectedResponse) + assert.Equal(t, resp, map[string]interface{}{"status": "success"}) assert.Empty(t, err) }) } -type MockCallRPC struct { - mock.Mock -} - -func (m *MockCallRPC) callRPC(rpcUrl string, method string, params map[string]string) (map[string]interface{}, error) { - args := m.Called(rpcUrl, method, params) - if args.Get(0) == nil { - return nil, args.Error(1) - } - return args.Get(0).(map[string]interface{}), args.Error(1) -} - -type MockUnMarshalErrorResultXdr struct { - mock.Mock -} - -func (m *MockUnMarshalErrorResultXdr) UnMarshalErrorResultXdr(errorResultXdr string) (tss.RPCTXCode, error) { - args := m.Called(errorResultXdr) - return args.Get(0).(tss.RPCTXCode), args.Error(1) -} - func TestSendTransaction(t *testing.T) { - mockCallRPC := MockCallRPC{} - callRPC = mockCallRPC.callRPC - defer func() { callRPC = sendRPCRequest }() - mockUnMarshalErrorResultXdr := MockUnMarshalErrorResultXdr{} - UnMarshalErrorResultXdr = mockUnMarshalErrorResultXdr.UnMarshalErrorResultXdr - defer func() { UnMarshalErrorResultXdr = parseErrorResultXdr }() + mockHTTPClient := MockHTTPClient{} + rpcURL := "http://localhost:8000/soroban/rpc" txService, _ := NewTransactionService(TransactionServiceOptions{ DistributionAccountSignatureClient: &signing.SignatureClientMock{}, ChannelAccountSignatureClient: &signing.SignatureClientMock{}, HorizonClient: &horizonclient.MockClient{}, - RpcUrl: "http://localhost:8000/soroban/rpc", + RPCURL: rpcURL, BaseFee: 114, + HTTPClient: &mockHTTPClient, + }) + method := "sendTransaction" + params := map[string]string{"transaction": "ABCD"} + payload := map[string]interface{}{ + "jsonrpc": "2.0", + "id": 1, + "method": method, + "params": params, + } + jsonData, _ := json.Marshal(payload) + + t.Run("rpc_request_fails", func(t *testing.T) { + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(&http.Response{}, errors.New("RPC Connection fail")). + Once() + + resp, err := txService.SendTransaction("ABCD") + + assert.Equal(t, tss.RPCFailCode, resp.Code.OtherCodes) + assert.Equal(t, "sendTransaction: sending POST request to rpc: RPC Connection fail", err.Error()) + }) - txXdr, _ := buildTestTransaction().Base64() - rpcUrl := "http://localhost:8000/soroban/rpc" - t.Run("call_rpc_returns_error", func(t *testing.T) { - mockCallRPC. - On("callRPC", rpcUrl, "sendTransaction", map[string]string{"transaction": txXdr}). - Return(nil, errors.New("unable to reach rpc server")). + t.Run("response_has_no_result_field", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"foo": "bar"}`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). Once() - rpcSendTxResponse, err := txService.SendTransaction(txXdr) - assert.Equal(t, rpcSendTxResponse.Code.OtherCodes, tss.RPCFailCode) - assert.Equal(t, "unable to reach rpc server", err.Error()) + resp, err := txService.SendTransaction("ABCD") + + assert.Equal(t, tss.RPCFailCode, resp.Code.OtherCodes) + assert.Equal(t, "RPC response has no result field", err.Error()) + }) - t.Run("error_unmarshaling_error_result_xdr", func(t *testing.T) { - errorResultXdr := "AAAAAAAAAGT////7AAAAAA==" - rpcResponse := map[string]interface{}{"jsonrpc": "2.0", "id": 1, "result": map[string]interface{}{"status": tss.ErrorStatus, "errorResultXdr": errorResultXdr}} - mockCallRPC. - On("callRPC", rpcUrl, "sendTransaction", map[string]string{"transaction": txXdr}). - Return(rpcResponse, nil). + + t.Run("response_has_status_field", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"result": {"status": "PENDING"}}`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). Once() - mockUnMarshalErrorResultXdr. - On("UnMarshalErrorResultXdr", errorResultXdr). - Return(tss.RPCTXCode{OtherCodes: tss.UnMarshalBinaryCode}, errors.New("unable to unmarshal")). + resp, err := txService.SendTransaction("ABCD") + + assert.Equal(t, tss.PendingStatus, resp.Status) + assert.Empty(t, err) + }) + + t.Run("response_has_hash_field", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"result": {"hash": "xyz"}}`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). Once() - rpcSendTxResponse, err := txService.SendTransaction(txXdr) - assert.Equal(t, rpcSendTxResponse.Status, tss.ErrorStatus) - assert.Equal(t, rpcSendTxResponse.Code.OtherCodes, tss.UnMarshalBinaryCode) - assert.Equal(t, "unable to unmarshal", err.Error()) - }) - t.Run("return_send_tx_response", func(t *testing.T) { - errorResultXdr := "AAAAAAAAAGT////7AAAAAA==" - rpcResponse := map[string]interface{}{"jsonrpc": "2.0", "id": 1, "result": map[string]interface{}{"status": tss.ErrorStatus, "errorResultXdr": errorResultXdr}} - mockCallRPC. - On("callRPC", rpcUrl, "sendTransaction", map[string]string{"transaction": txXdr}). - Return(rpcResponse, nil). + resp, err := txService.SendTransaction("ABCD") + + assert.Equal(t, "xyz", resp.TransactionHash) + assert.Empty(t, err) + }) + + t.Run("response_has_unparsable_errorResultXdr", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"result": {"errorResultXdr": "ABC123"}}`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). Once() - mockUnMarshalErrorResultXdr. - On("UnMarshalErrorResultXdr", errorResultXdr). - Return(tss.RPCTXCode{TxResultCode: xdr.TransactionResultCodeTxSuccess}, nil). + resp, err := txService.SendTransaction("ABCD") + + assert.Equal(t, tss.UnMarshalBinaryCode, resp.Code.OtherCodes) + assert.Equal(t, "unable to unmarshal errorResultXdr: ABC123", err.Error()) + }) + t.Run("response_has_errorResultXdr", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"result": {"errorResultXdr": "AAAAAAAAAMj////9AAAAAA=="}}`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). Once() - rpcSendTxResponse, err := txService.SendTransaction(txXdr) - assert.Equal(t, rpcSendTxResponse.Status, tss.ErrorStatus) - assert.Equal(t, rpcSendTxResponse.Code.TxResultCode, xdr.TransactionResultCodeTxSuccess) + resp, err := txService.SendTransaction("ABCD") + + assert.Equal(t, xdr.TransactionResultCodeTxTooLate, resp.Code.TxResultCode) assert.Empty(t, err) }) } func TestGetTransaction(t *testing.T) { - mockCallRPC := MockCallRPC{} - callRPC = mockCallRPC.callRPC - defer func() { callRPC = sendRPCRequest }() + mockHTTPClient := MockHTTPClient{} + rpcURL := "http://localhost:8000/soroban/rpc" txService, _ := NewTransactionService(TransactionServiceOptions{ DistributionAccountSignatureClient: &signing.SignatureClientMock{}, ChannelAccountSignatureClient: &signing.SignatureClientMock{}, HorizonClient: &horizonclient.MockClient{}, - RpcUrl: "http://localhost:8000/soroban/rpc", + RPCURL: rpcURL, BaseFee: 114, + HTTPClient: &mockHTTPClient, }) - txHash, _ := buildTestTransaction().HashHex("abcd") - rpcUrl := "http://localhost:8000/soroban/rpc" + method := "getTransaction" + params := map[string]string{"hash": "XYZ"} + payload := map[string]interface{}{ + "jsonrpc": "2.0", + "id": 1, + "method": method, + "params": params, + } + jsonData, _ := json.Marshal(payload) + + t.Run("rpc_request_fails", func(t *testing.T) { + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(&http.Response{}, errors.New("RPC Connection fail")). + Once() + + resp, err := txService.GetTransaction("XYZ") - t.Run("call_rpc_returns_error", func(t *testing.T) { - mockCallRPC. - On("callRPC", rpcUrl, "getTransaction", map[string]string{"hash": txHash}). - Return(nil, errors.New("unable to reach rpc server")). + assert.Equal(t, tss.ErrorStatus, resp.Status) + assert.Equal(t, "getTransaction: sending POST request to rpc: RPC Connection fail", err.Error()) + + }) + + t.Run("response_has_no_result_field", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"foo": "bar"}`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). Once() - _, err := txService.GetTransaction(txHash) - assert.Equal(t, "unable to reach rpc server", err.Error()) + resp, err := txService.GetTransaction("XYZ") + + assert.Equal(t, tss.ErrorStatus, resp.Status) + assert.Equal(t, "RPC response has no result field", err.Error()) }) - t.Run("returns_resp_with_status", func(t *testing.T) { - rpcResponse := map[string]interface{}{"jsonrpc": "2.0", "id": 1, "result": map[string]interface{}{"status": tss.SuccessStatus}} - mockCallRPC. - On("callRPC", rpcUrl, "getTransaction", map[string]string{"hash": txHash}). - Return(rpcResponse, nil). + t.Run("response_has_status_field", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"result": {"status": "SUCCESS"}}`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). Once() - getIngestTxResponse, err := txService.GetTransaction(txHash) - assert.Equal(t, getIngestTxResponse.Status, tss.SuccessStatus) - assert.Empty(t, getIngestTxResponse.EnvelopeXDR) - assert.Empty(t, getIngestTxResponse.ResultXDR) - assert.Empty(t, getIngestTxResponse.CreatedAt) + resp, err := txService.GetTransaction("XYZ") + + assert.Equal(t, tss.SuccessStatus, resp.Status) assert.Empty(t, err) }) - t.Run("returns_resp_with_envelope_xdr", func(t *testing.T) { - rpcResponse := map[string]interface{}{"jsonrpc": "2.0", "id": 1, "result": map[string]interface{}{"envelopeXdr": "abcd"}} - mockCallRPC. - On("callRPC", rpcUrl, "getTransaction", map[string]string{"hash": txHash}). - Return(rpcResponse, nil). + t.Run("response_has_envelopeXdr_field", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"result": {"envelopeXdr": "envelopeABCD"}}`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). Once() - getIngestTxResponse, err := txService.GetTransaction(txHash) - assert.Empty(t, getIngestTxResponse.Status) - assert.Equal(t, getIngestTxResponse.EnvelopeXDR, "abcd") - assert.Empty(t, getIngestTxResponse.ResultXDR) - assert.Empty(t, getIngestTxResponse.CreatedAt) + resp, err := txService.GetTransaction("XYZ") + + assert.Equal(t, "envelopeABCD", resp.EnvelopeXDR) assert.Empty(t, err) }) - t.Run("returns_resp_with_result_xdr", func(t *testing.T) { - rpcResponse := map[string]interface{}{"jsonrpc": "2.0", "id": 1, "result": map[string]interface{}{"resultXdr": "abcd"}} - mockCallRPC. - On("callRPC", rpcUrl, "getTransaction", map[string]string{"hash": txHash}). - Return(rpcResponse, nil). + t.Run("response_has_resultXdr_field", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"result": {"resultXdr": "resultABCD"}}`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). Once() - getIngestTxResponse, err := txService.GetTransaction(txHash) - assert.Empty(t, getIngestTxResponse.Status) - assert.Empty(t, getIngestTxResponse.EnvelopeXDR) - assert.Equal(t, getIngestTxResponse.ResultXDR, "abcd") - assert.Empty(t, getIngestTxResponse.CreatedAt) + resp, err := txService.GetTransaction("XYZ") + + assert.Equal(t, "resultABCD", resp.ResultXDR) assert.Empty(t, err) }) - t.Run("returns_resp_with_created_at", func(t *testing.T) { - rpcResponse := map[string]interface{}{"jsonrpc": "2.0", "id": 1, "result": map[string]interface{}{"createdAt": "1234"}} - mockCallRPC. - On("callRPC", rpcUrl, "getTransaction", map[string]string{"hash": txHash}). - Return(rpcResponse, nil). + t.Run("unable_to_parse_createdAt", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"result": {"createdAt": "ABCD"}}`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). Once() - getIngestTxResponse, err := txService.GetTransaction(txHash) - assert.Empty(t, getIngestTxResponse.Status) - assert.Empty(t, getIngestTxResponse.EnvelopeXDR) - assert.Empty(t, getIngestTxResponse.ResultXDR) - assert.Equal(t, getIngestTxResponse.CreatedAt, int64(1234)) + resp, err := txService.GetTransaction("XYZ") + + assert.Equal(t, tss.ErrorStatus, resp.Status) + assert.Equal(t, "cannot parse createdAt", err.Error()) + }) + + t.Run("response_has_createdAt_field", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"result": {"createdAt": "1234567"}}`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). + Once() + + resp, err := txService.GetTransaction("XYZ") + + assert.Equal(t, int64(1234567), resp.CreatedAt) assert.Empty(t, err) }) + } diff --git a/internal/tss/utils/types.go b/internal/tss/utils/types.go deleted file mode 100644 index 1cec932..0000000 --- a/internal/tss/utils/types.go +++ /dev/null @@ -1,13 +0,0 @@ -package utils - -import ( - "github.com/stellar/go/txnbuild" - "github.com/stellar/wallet-backend/internal/tss" -) - -type TransactionService interface { - NetworkPassPhrase() string - SignAndBuildNewTransaction(origTxXdr string) (*txnbuild.FeeBumpTransaction, error) - SendTransaction(transactionXdr string) (tss.RPCSendTxResponse, error) - GetTransaction(transactionHash string) (tss.RPCGetIngestTxResponse, error) -} From da2b40b2fc59b14883e04320b95880441c445814 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Thu, 19 Sep 2024 11:12:04 -0700 Subject: [PATCH 038/113] removing print statements --- internal/tss/channels/error_handler_service_jitter_channel.go | 2 -- internal/tss/channels/utils.go | 1 - internal/tss/services/error_handler_service.go | 3 --- 3 files changed, 6 deletions(-) diff --git a/internal/tss/channels/error_handler_service_jitter_channel.go b/internal/tss/channels/error_handler_service_jitter_channel.go index 9c4a5b1..346493b 100644 --- a/internal/tss/channels/error_handler_service_jitter_channel.go +++ b/internal/tss/channels/error_handler_service_jitter_channel.go @@ -2,7 +2,6 @@ package channels import ( "context" - "fmt" "slices" "time" @@ -61,7 +60,6 @@ func (p *rpcErrorHandlerServiceJitterPool) Receive(payload tss.Payload) { ctx := context.Background() var i int for i = 0; i < p.MaxRetries; i++ { - fmt.Println(i) currentBackoff := p.MinWaitBtwnRetriesMS * (1 << i) sleep(jitter(time.Duration(currentBackoff)) * time.Microsecond) rpcSendResp, err := SignAndSubmitTransaction(ctx, "ErrorHandlerServiceJitterChannel", payload, p.Store, p.TxService) diff --git a/internal/tss/channels/utils.go b/internal/tss/channels/utils.go index 4f87ff2..3db70da 100644 --- a/internal/tss/channels/utils.go +++ b/internal/tss/channels/utils.go @@ -12,7 +12,6 @@ import ( func SignAndSubmitTransaction(ctx context.Context, channelName string, payload tss.Payload, store store.Store, txService utils.TransactionService) (tss.RPCSendTxResponse, error) { feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(ctx, payload.TransactionXDR) if err != nil { - fmt.Println("JERE") return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to sign/build transaction: %s", channelName, err.Error()) } feeBumpTxHash, err := feeBumpTx.HashHex(txService.NetworkPassphrase()) diff --git a/internal/tss/services/error_handler_service.go b/internal/tss/services/error_handler_service.go index 2b50cd4..e24c310 100644 --- a/internal/tss/services/error_handler_service.go +++ b/internal/tss/services/error_handler_service.go @@ -1,7 +1,6 @@ package services import ( - "fmt" "slices" "github.com/stellar/wallet-backend/internal/tss" @@ -26,8 +25,6 @@ func NewErrorHandlerService(cfg ErrorHandlerServiceConfigs) *errorHandlerService func (p *errorHandlerService) ProcessPayload(payload tss.Payload) { if payload.RpcSubmitTxResponse.Status == tss.TryAgainLaterStatus { - fmt.Println("TRY AGAIN LATER") - fmt.Println(payload) p.JitterChannel.Send(payload) } else { if slices.Contains(tss.NonJitterErrorCodes, payload.RpcSubmitTxResponse.Code.TxResultCode) { From b71e342a74d0d1b9ee57119b895164503a385de7 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Thu, 19 Sep 2024 14:39:59 -0700 Subject: [PATCH 039/113] Update transaction_service.go --- internal/tss/utils/transaction_service.go | 62 +++++++++++------------ 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/internal/tss/utils/transaction_service.go b/internal/tss/utils/transaction_service.go index 164d0f4..243f014 100644 --- a/internal/tss/utils/transaction_service.go +++ b/internal/tss/utils/transaction_service.go @@ -93,37 +93,6 @@ func NewTransactionService(opts TransactionServiceOptions) (*transactionService, }, nil } -func (t *transactionService) sendRPCRequest(method string, params map[string]string) (map[string]interface{}, error) { - payload := map[string]interface{}{ - "jsonrpc": "2.0", - "id": 1, - "method": method, - "params": params, - } - jsonData, err := json.Marshal(payload) - - if err != nil { - return nil, fmt.Errorf("marshaling payload") - } - - resp, err := t.HTTPClient.Post(t.RPCURL, "application/json", bytes.NewBuffer(jsonData)) - if err != nil { - return nil, fmt.Errorf("%s: sending POST request to rpc: %v", method, err) - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("%s: unmarshaling RPC response", method) - } - var res map[string]interface{} - err = json.Unmarshal(body, &res) - if err != nil { - return nil, fmt.Errorf("%s: parsing RPC response JSON", method) - } - return res, nil -} - func (t *transactionService) SignAndBuildNewFeeBumpTransaction(ctx context.Context, origTxXdr string) (*txnbuild.FeeBumpTransaction, error) { genericTx, err := txnbuild.TransactionFromXDR(origTxXdr) if err != nil { @@ -205,6 +174,37 @@ func (t *transactionService) parseErrorResultXDR(errorResultXdr string) (tss.RPC }, nil } +func (t *transactionService) sendRPCRequest(method string, params map[string]string) (map[string]interface{}, error) { + payload := map[string]interface{}{ + "jsonrpc": "2.0", + "id": 1, + "method": method, + "params": params, + } + jsonData, err := json.Marshal(payload) + + if err != nil { + return nil, fmt.Errorf("marshaling payload") + } + + resp, err := t.HTTPClient.Post(t.RPCURL, "application/json", bytes.NewBuffer(jsonData)) + if err != nil { + return nil, fmt.Errorf("%s: sending POST request to rpc: %v", method, err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("%s: unmarshaling RPC response", method) + } + var res map[string]interface{} + err = json.Unmarshal(body, &res) + if err != nil { + return nil, fmt.Errorf("%s: parsing RPC response JSON", method) + } + return res, nil +} + func (t *transactionService) SendTransaction(transactionXdr string) (tss.RPCSendTxResponse, error) { rpcResponse, err := t.sendRPCRequest("sendTransaction", map[string]string{"transaction": transactionXdr}) sendTxResponse := tss.RPCSendTxResponse{} From ccf27f1886d5f46e0a42befc388ac70e22c199f1 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Thu, 19 Sep 2024 18:20:25 -0700 Subject: [PATCH 040/113] responding to comments --- internal/tss/types.go | 13 ++ internal/tss/utils/transaction_builder.go | 18 +- internal/tss/utils/transaction_service.go | 87 +++------ .../tss/utils/transaction_service_test.go | 174 ++++++++---------- 4 files changed, 127 insertions(+), 165 deletions(-) diff --git a/internal/tss/types.go b/internal/tss/types.go index 3f055f4..9d5f6a4 100644 --- a/internal/tss/types.go +++ b/internal/tss/types.go @@ -69,6 +69,19 @@ type Payload struct { RpcGetIngestTxResponse RPCGetIngestTxResponse } +type RPCResult struct { + Status string `json:"status"` + EnvelopeXDR string `json:"envelopeXdr"` + ResultXDR string `json:"resultXdr"` + ErrorResultXDR string `json:"errorResultXdr"` + Hash string `json:"hash"` + CreatedAt string `json:"createdAt"` +} + +type RPCResponse struct { + RPCResult `json:"result"` +} + type Channel interface { Send(payload Payload) Receive(payload Payload) diff --git a/internal/tss/utils/transaction_builder.go b/internal/tss/utils/transaction_builder.go index 8347200..c068a0e 100644 --- a/internal/tss/utils/transaction_builder.go +++ b/internal/tss/utils/transaction_builder.go @@ -1,9 +1,9 @@ package utils import ( + "bytes" "encoding/base64" "fmt" - "strings" xdr3 "github.com/stellar/go-xdr/xdr3" "github.com/stellar/go/keypair" @@ -18,9 +18,12 @@ func BuildOriginalTransaction(txOpXDRs []string) (*txnbuild.Transaction, error) if err != nil { return nil, fmt.Errorf("decoding Operation XDR string") } - dec := xdr3.NewDecoder(strings.NewReader(string(decodedBytes))) + //dec := xdr3.NewDecoder(strings.NewReader(string(decodedBytes))) var decodedOp xdr.Operation - _, err = dec.Decode(&decodedOp) + //_, err = dec.Decode(&decodedOp) + + _, err = xdr3.Unmarshal(bytes.NewReader(decodedBytes), &decodedOp) + if err != nil { return nil, fmt.Errorf("decoding xdr into xdr Operation: %w", err) } @@ -40,12 +43,11 @@ func BuildOriginalTransaction(txOpXDRs []string) (*txnbuild.Transaction, error) tx, _ := txnbuild.NewTransaction(txnbuild.TransactionParams{ SourceAccount: &txnbuild.SimpleAccount{ AccountID: keypair.MustRandom().Address(), - Sequence: 123, }, - IncrementSequenceNum: true, - Operations: operations, - BaseFee: 104, - Preconditions: txnbuild.Preconditions{TimeBounds: txnbuild.NewTimeout(10)}, + //IncrementSequenceNum: true, + Operations: operations, + BaseFee: 104, + Preconditions: txnbuild.Preconditions{TimeBounds: txnbuild.NewTimeout(10)}, }) return tx, nil } diff --git a/internal/tss/utils/transaction_service.go b/internal/tss/utils/transaction_service.go index 243f014..9cc9c8e 100644 --- a/internal/tss/utils/transaction_service.go +++ b/internal/tss/utils/transaction_service.go @@ -9,7 +9,6 @@ import ( "io" "net/http" "strconv" - "strings" xdr3 "github.com/stellar/go-xdr/xdr3" "github.com/stellar/go/clients/horizonclient" @@ -37,7 +36,6 @@ type transactionService struct { RPCURL string BaseFee int64 HTTPClient HTTPClient - Ctx context.Context } var _ TransactionService = (*transactionService)(nil) @@ -116,7 +114,7 @@ func (t *transactionService) SignAndBuildNewFeeBumpTransaction(ctx context.Conte Operations: originalTx.Operations(), BaseFee: int64(t.BaseFee), Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewTimeout(10), + TimeBounds: txnbuild.NewTimeout(300), }, IncrementSequenceNum: true, }, @@ -153,28 +151,22 @@ func (t *transactionService) SignAndBuildNewFeeBumpTransaction(ctx context.Conte } func (t *transactionService) parseErrorResultXDR(errorResultXdr string) (tss.RPCTXCode, error) { - - //errorResult := xdr.TransactionResult{} - unMarshallErr := "unable to unmarshal errorResultXdr: %s" - //err := errorResult.UnmarshalBinary([]byte(errorResultXdr)) - + unMarshalErr := "unable to unmarshal errorResultXdr: %s" decodedBytes, err := base64.StdEncoding.DecodeString(errorResultXdr) if err != nil { - return tss.RPCTXCode{OtherCodes: tss.UnMarshalBinaryCode}, fmt.Errorf(unMarshallErr, errorResultXdr) + return tss.RPCTXCode{OtherCodes: tss.UnMarshalBinaryCode}, fmt.Errorf(unMarshalErr, errorResultXdr) } - dec := xdr3.NewDecoder(strings.NewReader(string(decodedBytes))) var errorResult xdr.TransactionResult - _, err = dec.Decode(&errorResult) - + _, err = xdr3.Unmarshal(bytes.NewReader(decodedBytes), &errorResult) if err != nil { - return tss.RPCTXCode{OtherCodes: tss.UnMarshalBinaryCode}, fmt.Errorf(unMarshallErr, errorResultXdr) + return tss.RPCTXCode{OtherCodes: tss.UnMarshalBinaryCode}, fmt.Errorf(unMarshalErr, errorResultXdr) } return tss.RPCTXCode{ TxResultCode: errorResult.Result.Code, }, nil } -func (t *transactionService) sendRPCRequest(method string, params map[string]string) (map[string]interface{}, error) { +func (t *transactionService) sendRPCRequest(method string, params map[string]string) (tss.RPCResponse, error) { payload := map[string]interface{}{ "jsonrpc": "2.0", "id": 1, @@ -184,23 +176,26 @@ func (t *transactionService) sendRPCRequest(method string, params map[string]str jsonData, err := json.Marshal(payload) if err != nil { - return nil, fmt.Errorf("marshaling payload") + return tss.RPCResponse{}, fmt.Errorf("marshaling payload") } resp, err := t.HTTPClient.Post(t.RPCURL, "application/json", bytes.NewBuffer(jsonData)) if err != nil { - return nil, fmt.Errorf("%s: sending POST request to rpc: %v", method, err) + return tss.RPCResponse{}, fmt.Errorf("%s: sending POST request to rpc: %v", method, err) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { - return nil, fmt.Errorf("%s: unmarshaling RPC response", method) + return tss.RPCResponse{}, fmt.Errorf("%s: unmarshaling RPC response", method) } - var res map[string]interface{} + var res tss.RPCResponse err = json.Unmarshal(body, &res) if err != nil { - return nil, fmt.Errorf("%s: parsing RPC response JSON", method) + return tss.RPCResponse{}, fmt.Errorf("%s: parsing RPC response JSON", method) + } + if res.RPCResult == (tss.RPCResult{}) { + return tss.RPCResponse{}, fmt.Errorf("%s: response missing result field", method) } return res, nil } @@ -211,56 +206,28 @@ func (t *transactionService) SendTransaction(transactionXdr string) (tss.RPCSend sendTxResponse.TransactionXDR = transactionXdr if err != nil { sendTxResponse.Code.OtherCodes = tss.RPCFailCode - return sendTxResponse, fmt.Errorf(err.Error()) - } - - if result, ok := rpcResponse["result"].(map[string]interface{}); ok { - if val, exists := result["status"].(string); exists { - sendTxResponse.Status = tss.RPCTXStatus(val) - } - if val, exists := result["errorResultXdr"].(string); exists { - sendTxResponse.Code, err = t.parseErrorResultXDR(val) - } - if hash, exists := result["hash"].(string); exists { - sendTxResponse.TransactionHash = hash - } - } else { - sendTxResponse.Code.OtherCodes = tss.RPCFailCode - return sendTxResponse, fmt.Errorf("RPC response has no result field") + return sendTxResponse, fmt.Errorf("RPC fail: %s", err.Error()) } + sendTxResponse.Status = tss.RPCTXStatus(rpcResponse.RPCResult.Status) + sendTxResponse.Code, err = t.parseErrorResultXDR(rpcResponse.RPCResult.ErrorResultXDR) + sendTxResponse.TransactionHash = rpcResponse.RPCResult.Hash return sendTxResponse, err } func (t *transactionService) GetTransaction(transactionHash string) (tss.RPCGetIngestTxResponse, error) { rpcResponse, err := t.sendRPCRequest("getTransaction", map[string]string{"hash": transactionHash}) if err != nil { - return tss.RPCGetIngestTxResponse{Status: tss.ErrorStatus}, fmt.Errorf(err.Error()) + return tss.RPCGetIngestTxResponse{Status: tss.ErrorStatus}, fmt.Errorf("RPC Fail: %s", err.Error()) } - getIngestTxResponse := tss.RPCGetIngestTxResponse{} - if result, ok := rpcResponse["result"].(map[string]interface{}); ok { - if status, exists := result["status"].(string); exists { - getIngestTxResponse.Status = tss.RPCTXStatus(status) - } - if envelopeXDR, exists := result["envelopeXdr"].(string); exists { - getIngestTxResponse.EnvelopeXDR = envelopeXDR + getIngestTxResponse.Status = tss.RPCTXStatus(rpcResponse.RPCResult.Status) + getIngestTxResponse.EnvelopeXDR = rpcResponse.RPCResult.EnvelopeXDR + getIngestTxResponse.ResultXDR = rpcResponse.RPCResult.ResultXDR + if getIngestTxResponse.Status != tss.NotFoundStatus { + getIngestTxResponse.CreatedAt, err = strconv.ParseInt(rpcResponse.RPCResult.CreatedAt, 10, 64) + if err != nil { + return tss.RPCGetIngestTxResponse{Status: tss.ErrorStatus}, fmt.Errorf("unable to parse createAt: %s", err.Error()) } - if resultXDR, exists := result["resultXdr"].(string); exists { - getIngestTxResponse.ResultXDR = resultXDR - } - if createdAt, exists := result["createdAt"].(string); exists { - createdAtInt, e := strconv.ParseInt(createdAt, 10, 64) - if e != nil { - getIngestTxResponse.Status = tss.ErrorStatus - err = fmt.Errorf("cannot parse createdAt") - } else { - getIngestTxResponse.CreatedAt = createdAtInt - } - } - } else { - getIngestTxResponse.Status = tss.ErrorStatus - return getIngestTxResponse, fmt.Errorf("RPC response has no result field") - } - return getIngestTxResponse, err + return getIngestTxResponse, nil } diff --git a/internal/tss/utils/transaction_service_test.go b/internal/tss/utils/transaction_service_test.go index f58d2fd..8a94f86 100644 --- a/internal/tss/utils/transaction_service_test.go +++ b/internal/tss/utils/transaction_service_test.go @@ -373,7 +373,7 @@ func TestSendRPCRequest(t *testing.T) { assert.Equal(t, "sendTransaction: parsing RPC response JSON", err.Error()) }) - t.Run("returns_rpc_response", func(t *testing.T) { + t.Run("response_has_no_result_field", func(t *testing.T) { httpResponse := &http.Response{ StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(`{"status": "success"}`)), @@ -385,127 +385,108 @@ func TestSendRPCRequest(t *testing.T) { resp, err := txService.sendRPCRequest(method, params) - assert.Equal(t, resp, map[string]interface{}{"status": "success"}) - assert.Empty(t, err) - }) -} - -func TestSendTransaction(t *testing.T) { - mockHTTPClient := MockHTTPClient{} - rpcURL := "http://localhost:8000/soroban/rpc" - txService, _ := NewTransactionService(TransactionServiceOptions{ - DistributionAccountSignatureClient: &signing.SignatureClientMock{}, - ChannelAccountSignatureClient: &signing.SignatureClientMock{}, - HorizonClient: &horizonclient.MockClient{}, - RPCURL: rpcURL, - BaseFee: 114, - HTTPClient: &mockHTTPClient, + assert.Empty(t, resp) + assert.Equal(t, "sendTransaction: response missing result field", err.Error()) }) - method := "sendTransaction" - params := map[string]string{"transaction": "ABCD"} - payload := map[string]interface{}{ - "jsonrpc": "2.0", - "id": 1, - "method": method, - "params": params, - } - jsonData, _ := json.Marshal(payload) - t.Run("rpc_request_fails", func(t *testing.T) { + t.Run("response_has_status_field", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"result": {"status": "PENDING"}}`)), + } mockHTTPClient. On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(&http.Response{}, errors.New("RPC Connection fail")). + Return(httpResponse, nil). Once() - resp, err := txService.SendTransaction("ABCD") - - assert.Equal(t, tss.RPCFailCode, resp.Code.OtherCodes) - assert.Equal(t, "sendTransaction: sending POST request to rpc: RPC Connection fail", err.Error()) + resp, err := txService.sendRPCRequest(method, params) + assert.Equal(t, "PENDING", resp.Status) + assert.Empty(t, err) }) - t.Run("response_has_no_result_field", func(t *testing.T) { + t.Run("response_has_envelopexdr_field", func(t *testing.T) { httpResponse := &http.Response{ StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"foo": "bar"}`)), + Body: io.NopCloser(strings.NewReader(`{"result": {"envelopeXdr": "exdr"}}`)), } mockHTTPClient. On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). Return(httpResponse, nil). Once() - resp, err := txService.SendTransaction("ABCD") - - assert.Equal(t, tss.RPCFailCode, resp.Code.OtherCodes) - assert.Equal(t, "RPC response has no result field", err.Error()) + resp, err := txService.sendRPCRequest(method, params) + assert.Equal(t, "exdr", resp.EnvelopeXDR) + assert.Empty(t, err) }) - t.Run("response_has_status_field", func(t *testing.T) { + t.Run("response_has_resultxdr_field", func(t *testing.T) { httpResponse := &http.Response{ StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"status": "PENDING"}}`)), + Body: io.NopCloser(strings.NewReader(`{"result": {"resultXdr": "rxdr"}}`)), } mockHTTPClient. On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). Return(httpResponse, nil). Once() - resp, err := txService.SendTransaction("ABCD") + resp, err := txService.sendRPCRequest(method, params) - assert.Equal(t, tss.PendingStatus, resp.Status) + assert.Equal(t, "rxdr", resp.ResultXDR) assert.Empty(t, err) }) - t.Run("response_has_hash_field", func(t *testing.T) { + t.Run("response_has_errorresultxdr_field", func(t *testing.T) { httpResponse := &http.Response{ StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"hash": "xyz"}}`)), + Body: io.NopCloser(strings.NewReader(`{"result": {"errorResultXdr": "exdr"}}`)), } mockHTTPClient. On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). Return(httpResponse, nil). Once() - resp, err := txService.SendTransaction("ABCD") + resp, err := txService.sendRPCRequest(method, params) - assert.Equal(t, "xyz", resp.TransactionHash) + assert.Equal(t, "exdr", resp.ErrorResultXDR) assert.Empty(t, err) }) - t.Run("response_has_unparsable_errorResultXdr", func(t *testing.T) { + t.Run("response_has_hash_field", func(t *testing.T) { httpResponse := &http.Response{ StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"errorResultXdr": "ABC123"}}`)), + Body: io.NopCloser(strings.NewReader(`{"result": {"hash": "hash"}}`)), } mockHTTPClient. On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). Return(httpResponse, nil). Once() - resp, err := txService.SendTransaction("ABCD") + resp, err := txService.sendRPCRequest(method, params) - assert.Equal(t, tss.UnMarshalBinaryCode, resp.Code.OtherCodes) - assert.Equal(t, "unable to unmarshal errorResultXdr: ABC123", err.Error()) + assert.Equal(t, "hash", resp.Hash) + assert.Empty(t, err) }) - t.Run("response_has_errorResultXdr", func(t *testing.T) { + + t.Run("response_has_createdat_field", func(t *testing.T) { httpResponse := &http.Response{ StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"errorResultXdr": "AAAAAAAAAMj////9AAAAAA=="}}`)), + Body: io.NopCloser(strings.NewReader(`{"result": {"createdAt": "1234"}}`)), } mockHTTPClient. On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). Return(httpResponse, nil). Once() - resp, err := txService.SendTransaction("ABCD") + resp, err := txService.sendRPCRequest(method, params) - assert.Equal(t, xdr.TransactionResultCodeTxTooLate, resp.Code.TxResultCode) + assert.Equal(t, "1234", resp.CreatedAt) assert.Empty(t, err) }) } -func TestGetTransaction(t *testing.T) { +func TestSendTransaction(t *testing.T) { mockHTTPClient := MockHTTPClient{} rpcURL := "http://localhost:8000/soroban/rpc" txService, _ := NewTransactionService(TransactionServiceOptions{ @@ -516,8 +497,8 @@ func TestGetTransaction(t *testing.T) { BaseFee: 114, HTTPClient: &mockHTTPClient, }) - method := "getTransaction" - params := map[string]string{"hash": "XYZ"} + method := "sendTransaction" + params := map[string]string{"transaction": "ABCD"} payload := map[string]interface{}{ "jsonrpc": "2.0", "id": 1, @@ -532,81 +513,81 @@ func TestGetTransaction(t *testing.T) { Return(&http.Response{}, errors.New("RPC Connection fail")). Once() - resp, err := txService.GetTransaction("XYZ") + resp, err := txService.SendTransaction("ABCD") - assert.Equal(t, tss.ErrorStatus, resp.Status) - assert.Equal(t, "getTransaction: sending POST request to rpc: RPC Connection fail", err.Error()) + assert.Equal(t, tss.RPCFailCode, resp.Code.OtherCodes) + assert.Equal(t, "RPC fail: sendTransaction: sending POST request to rpc: RPC Connection fail", err.Error()) }) - - t.Run("response_has_no_result_field", func(t *testing.T) { + t.Run("response_has_unparsable_errorResultXdr", func(t *testing.T) { httpResponse := &http.Response{ StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"foo": "bar"}`)), + Body: io.NopCloser(strings.NewReader(`{"result": {"errorResultXdr": "ABC123"}}`)), } mockHTTPClient. On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). Return(httpResponse, nil). Once() - resp, err := txService.GetTransaction("XYZ") + resp, err := txService.SendTransaction("ABCD") - assert.Equal(t, tss.ErrorStatus, resp.Status) - assert.Equal(t, "RPC response has no result field", err.Error()) + assert.Equal(t, tss.UnMarshalBinaryCode, resp.Code.OtherCodes) + assert.Equal(t, "unable to unmarshal errorResultXdr: ABC123", err.Error()) }) - - t.Run("response_has_status_field", func(t *testing.T) { + t.Run("response_has_errorResultXdr", func(t *testing.T) { httpResponse := &http.Response{ StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"status": "SUCCESS"}}`)), + Body: io.NopCloser(strings.NewReader(`{"result": {"errorResultXdr": "AAAAAAAAAMj////9AAAAAA=="}}`)), } mockHTTPClient. On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). Return(httpResponse, nil). Once() - resp, err := txService.GetTransaction("XYZ") + resp, err := txService.SendTransaction("ABCD") - assert.Equal(t, tss.SuccessStatus, resp.Status) + assert.Equal(t, xdr.TransactionResultCodeTxTooLate, resp.Code.TxResultCode) assert.Empty(t, err) }) +} - t.Run("response_has_envelopeXdr_field", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"envelopeXdr": "envelopeABCD"}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.GetTransaction("XYZ") - - assert.Equal(t, "envelopeABCD", resp.EnvelopeXDR) - assert.Empty(t, err) +func TestGetTransaction(t *testing.T) { + mockHTTPClient := MockHTTPClient{} + rpcURL := "http://localhost:8000/soroban/rpc" + txService, _ := NewTransactionService(TransactionServiceOptions{ + DistributionAccountSignatureClient: &signing.SignatureClientMock{}, + ChannelAccountSignatureClient: &signing.SignatureClientMock{}, + HorizonClient: &horizonclient.MockClient{}, + RPCURL: rpcURL, + BaseFee: 114, + HTTPClient: &mockHTTPClient, }) + method := "getTransaction" + params := map[string]string{"hash": "XYZ"} + payload := map[string]interface{}{ + "jsonrpc": "2.0", + "id": 1, + "method": method, + "params": params, + } + jsonData, _ := json.Marshal(payload) - t.Run("response_has_resultXdr_field", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"resultXdr": "resultABCD"}}`)), - } + t.Run("rpc_request_fails", func(t *testing.T) { mockHTTPClient. On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). + Return(&http.Response{}, errors.New("RPC Connection fail")). Once() resp, err := txService.GetTransaction("XYZ") - assert.Equal(t, "resultABCD", resp.ResultXDR) - assert.Empty(t, err) - }) + assert.Equal(t, tss.ErrorStatus, resp.Status) + assert.Equal(t, "RPC Fail: getTransaction: sending POST request to rpc: RPC Connection fail", err.Error()) + }) t.Run("unable_to_parse_createdAt", func(t *testing.T) { httpResponse := &http.Response{ StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"createdAt": "ABCD"}}`)), + Body: io.NopCloser(strings.NewReader(`{"result": {"status": "SUCCESS", "createdAt": "ABCD"}}`)), } mockHTTPClient. On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). @@ -616,9 +597,8 @@ func TestGetTransaction(t *testing.T) { resp, err := txService.GetTransaction("XYZ") assert.Equal(t, tss.ErrorStatus, resp.Status) - assert.Equal(t, "cannot parse createdAt", err.Error()) + assert.Equal(t, "unable to parse createAt: strconv.ParseInt: parsing \"ABCD\": invalid syntax", err.Error()) }) - t.Run("response_has_createdAt_field", func(t *testing.T) { httpResponse := &http.Response{ StatusCode: http.StatusOK, From 8045d8df43d853a1465befc9d99b445a805669e3 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Thu, 19 Sep 2024 20:07:58 -0700 Subject: [PATCH 041/113] remove commented code --- internal/tss/utils/transaction_builder.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/internal/tss/utils/transaction_builder.go b/internal/tss/utils/transaction_builder.go index c068a0e..38a3190 100644 --- a/internal/tss/utils/transaction_builder.go +++ b/internal/tss/utils/transaction_builder.go @@ -18,10 +18,7 @@ func BuildOriginalTransaction(txOpXDRs []string) (*txnbuild.Transaction, error) if err != nil { return nil, fmt.Errorf("decoding Operation XDR string") } - //dec := xdr3.NewDecoder(strings.NewReader(string(decodedBytes))) var decodedOp xdr.Operation - //_, err = dec.Decode(&decodedOp) - _, err = xdr3.Unmarshal(bytes.NewReader(decodedBytes), &decodedOp) if err != nil { From d062a1119df4f1c43bb426d9cb832c3bf7185ba5 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Thu, 19 Sep 2024 20:13:12 -0700 Subject: [PATCH 042/113] tx service changes --- internal/tss/types.go | 13 ++ internal/tss/utils/transaction_builder.go | 15 +- internal/tss/utils/transaction_service.go | 137 ++++++-------- .../tss/utils/transaction_service_test.go | 174 ++++++++---------- 4 files changed, 149 insertions(+), 190 deletions(-) diff --git a/internal/tss/types.go b/internal/tss/types.go index 3f055f4..9d5f6a4 100644 --- a/internal/tss/types.go +++ b/internal/tss/types.go @@ -69,6 +69,19 @@ type Payload struct { RpcGetIngestTxResponse RPCGetIngestTxResponse } +type RPCResult struct { + Status string `json:"status"` + EnvelopeXDR string `json:"envelopeXdr"` + ResultXDR string `json:"resultXdr"` + ErrorResultXDR string `json:"errorResultXdr"` + Hash string `json:"hash"` + CreatedAt string `json:"createdAt"` +} + +type RPCResponse struct { + RPCResult `json:"result"` +} + type Channel interface { Send(payload Payload) Receive(payload Payload) diff --git a/internal/tss/utils/transaction_builder.go b/internal/tss/utils/transaction_builder.go index 8347200..38a3190 100644 --- a/internal/tss/utils/transaction_builder.go +++ b/internal/tss/utils/transaction_builder.go @@ -1,9 +1,9 @@ package utils import ( + "bytes" "encoding/base64" "fmt" - "strings" xdr3 "github.com/stellar/go-xdr/xdr3" "github.com/stellar/go/keypair" @@ -18,9 +18,9 @@ func BuildOriginalTransaction(txOpXDRs []string) (*txnbuild.Transaction, error) if err != nil { return nil, fmt.Errorf("decoding Operation XDR string") } - dec := xdr3.NewDecoder(strings.NewReader(string(decodedBytes))) var decodedOp xdr.Operation - _, err = dec.Decode(&decodedOp) + _, err = xdr3.Unmarshal(bytes.NewReader(decodedBytes), &decodedOp) + if err != nil { return nil, fmt.Errorf("decoding xdr into xdr Operation: %w", err) } @@ -40,12 +40,11 @@ func BuildOriginalTransaction(txOpXDRs []string) (*txnbuild.Transaction, error) tx, _ := txnbuild.NewTransaction(txnbuild.TransactionParams{ SourceAccount: &txnbuild.SimpleAccount{ AccountID: keypair.MustRandom().Address(), - Sequence: 123, }, - IncrementSequenceNum: true, - Operations: operations, - BaseFee: 104, - Preconditions: txnbuild.Preconditions{TimeBounds: txnbuild.NewTimeout(10)}, + //IncrementSequenceNum: true, + Operations: operations, + BaseFee: 104, + Preconditions: txnbuild.Preconditions{TimeBounds: txnbuild.NewTimeout(10)}, }) return tx, nil } diff --git a/internal/tss/utils/transaction_service.go b/internal/tss/utils/transaction_service.go index 35f89a0..83e5643 100644 --- a/internal/tss/utils/transaction_service.go +++ b/internal/tss/utils/transaction_service.go @@ -9,7 +9,6 @@ import ( "io" "net/http" "strconv" - "strings" xdr3 "github.com/stellar/go-xdr/xdr3" "github.com/stellar/go/clients/horizonclient" @@ -38,7 +37,6 @@ type transactionService struct { RPCURL string BaseFee int64 HTTPClient HTTPClient - Ctx context.Context } var _ TransactionService = (*transactionService)(nil) @@ -94,37 +92,6 @@ func NewTransactionService(opts TransactionServiceOptions) (*transactionService, }, nil } -func (t *transactionService) sendRPCRequest(method string, params map[string]string) (map[string]interface{}, error) { - payload := map[string]interface{}{ - "jsonrpc": "2.0", - "id": 1, - "method": method, - "params": params, - } - jsonData, err := json.Marshal(payload) - - if err != nil { - return nil, fmt.Errorf("marshaling payload") - } - - resp, err := t.HTTPClient.Post(t.RPCURL, "application/json", bytes.NewBuffer(jsonData)) - if err != nil { - return nil, fmt.Errorf("%s: sending POST request to rpc: %v", method, err) - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("%s: unmarshaling RPC response", method) - } - var res map[string]interface{} - err = json.Unmarshal(body, &res) - if err != nil { - return nil, fmt.Errorf("%s: parsing RPC response JSON", method) - } - return res, nil -} - func (t *transactionService) NetworkPassphrase() string { return t.DistributionAccountSignatureClient.NetworkPassphrase() } @@ -152,7 +119,7 @@ func (t *transactionService) SignAndBuildNewFeeBumpTransaction(ctx context.Conte Operations: originalTx.Operations(), BaseFee: int64(t.BaseFee), Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewTimeout(10), + TimeBounds: txnbuild.NewTimeout(300), }, IncrementSequenceNum: true, }, @@ -189,83 +156,83 @@ func (t *transactionService) SignAndBuildNewFeeBumpTransaction(ctx context.Conte } func (t *transactionService) parseErrorResultXDR(errorResultXdr string) (tss.RPCTXCode, error) { - - //errorResult := xdr.TransactionResult{} - unMarshallErr := "unable to unmarshal errorResultXdr: %s" - //err := errorResult.UnmarshalBinary([]byte(errorResultXdr)) - + unMarshalErr := "unable to unmarshal errorResultXdr: %s" decodedBytes, err := base64.StdEncoding.DecodeString(errorResultXdr) if err != nil { - return tss.RPCTXCode{OtherCodes: tss.UnMarshalBinaryCode}, fmt.Errorf(unMarshallErr, errorResultXdr) + return tss.RPCTXCode{OtherCodes: tss.UnMarshalBinaryCode}, fmt.Errorf(unMarshalErr, errorResultXdr) } - dec := xdr3.NewDecoder(strings.NewReader(string(decodedBytes))) var errorResult xdr.TransactionResult - _, err = dec.Decode(&errorResult) - + _, err = xdr3.Unmarshal(bytes.NewReader(decodedBytes), &errorResult) if err != nil { - return tss.RPCTXCode{OtherCodes: tss.UnMarshalBinaryCode}, fmt.Errorf(unMarshallErr, errorResultXdr) + return tss.RPCTXCode{OtherCodes: tss.UnMarshalBinaryCode}, fmt.Errorf(unMarshalErr, errorResultXdr) } return tss.RPCTXCode{ TxResultCode: errorResult.Result.Code, }, nil } +func (t *transactionService) sendRPCRequest(method string, params map[string]string) (tss.RPCResponse, error) { + payload := map[string]interface{}{ + "jsonrpc": "2.0", + "id": 1, + "method": method, + "params": params, + } + jsonData, err := json.Marshal(payload) + + if err != nil { + return tss.RPCResponse{}, fmt.Errorf("marshaling payload") + } + + resp, err := t.HTTPClient.Post(t.RPCURL, "application/json", bytes.NewBuffer(jsonData)) + if err != nil { + return tss.RPCResponse{}, fmt.Errorf("%s: sending POST request to rpc: %v", method, err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return tss.RPCResponse{}, fmt.Errorf("%s: unmarshaling RPC response", method) + } + var res tss.RPCResponse + err = json.Unmarshal(body, &res) + if err != nil { + return tss.RPCResponse{}, fmt.Errorf("%s: parsing RPC response JSON", method) + } + if res.RPCResult == (tss.RPCResult{}) { + return tss.RPCResponse{}, fmt.Errorf("%s: response missing result field", method) + } + return res, nil +} + func (t *transactionService) SendTransaction(transactionXdr string) (tss.RPCSendTxResponse, error) { rpcResponse, err := t.sendRPCRequest("sendTransaction", map[string]string{"transaction": transactionXdr}) sendTxResponse := tss.RPCSendTxResponse{} sendTxResponse.TransactionXDR = transactionXdr if err != nil { sendTxResponse.Code.OtherCodes = tss.RPCFailCode - return sendTxResponse, fmt.Errorf(err.Error()) - } - - if result, ok := rpcResponse["result"].(map[string]interface{}); ok { - if val, exists := result["status"].(string); exists { - sendTxResponse.Status = tss.RPCTXStatus(val) - } - if val, exists := result["errorResultXdr"].(string); exists { - sendTxResponse.Code, err = t.parseErrorResultXDR(val) - } - if hash, exists := result["hash"].(string); exists { - sendTxResponse.TransactionHash = hash - } - } else { - sendTxResponse.Code.OtherCodes = tss.RPCFailCode - return sendTxResponse, fmt.Errorf("RPC response has no result field") + return sendTxResponse, fmt.Errorf("RPC fail: %s", err.Error()) } + sendTxResponse.Status = tss.RPCTXStatus(rpcResponse.RPCResult.Status) + sendTxResponse.Code, err = t.parseErrorResultXDR(rpcResponse.RPCResult.ErrorResultXDR) + sendTxResponse.TransactionHash = rpcResponse.RPCResult.Hash return sendTxResponse, err } func (t *transactionService) GetTransaction(transactionHash string) (tss.RPCGetIngestTxResponse, error) { rpcResponse, err := t.sendRPCRequest("getTransaction", map[string]string{"hash": transactionHash}) if err != nil { - return tss.RPCGetIngestTxResponse{Status: tss.ErrorStatus}, fmt.Errorf(err.Error()) + return tss.RPCGetIngestTxResponse{Status: tss.ErrorStatus}, fmt.Errorf("RPC Fail: %s", err.Error()) } - getIngestTxResponse := tss.RPCGetIngestTxResponse{} - if result, ok := rpcResponse["result"].(map[string]interface{}); ok { - if status, exists := result["status"].(string); exists { - getIngestTxResponse.Status = tss.RPCTXStatus(status) + getIngestTxResponse.Status = tss.RPCTXStatus(rpcResponse.RPCResult.Status) + getIngestTxResponse.EnvelopeXDR = rpcResponse.RPCResult.EnvelopeXDR + getIngestTxResponse.ResultXDR = rpcResponse.RPCResult.ResultXDR + if getIngestTxResponse.Status != tss.NotFoundStatus { + getIngestTxResponse.CreatedAt, err = strconv.ParseInt(rpcResponse.RPCResult.CreatedAt, 10, 64) + if err != nil { + return tss.RPCGetIngestTxResponse{Status: tss.ErrorStatus}, fmt.Errorf("unable to parse createAt: %s", err.Error()) } - if envelopeXDR, exists := result["envelopeXdr"].(string); exists { - getIngestTxResponse.EnvelopeXDR = envelopeXDR - } - if resultXDR, exists := result["resultXdr"].(string); exists { - getIngestTxResponse.ResultXDR = resultXDR - } - if createdAt, exists := result["createdAt"].(string); exists { - createdAtInt, e := strconv.ParseInt(createdAt, 10, 64) - if e != nil { - getIngestTxResponse.Status = tss.ErrorStatus - err = fmt.Errorf("cannot parse createdAt") - } else { - getIngestTxResponse.CreatedAt = createdAtInt - } - } - } else { - getIngestTxResponse.Status = tss.ErrorStatus - return getIngestTxResponse, fmt.Errorf("RPC response has no result field") - } - return getIngestTxResponse, err + return getIngestTxResponse, nil } diff --git a/internal/tss/utils/transaction_service_test.go b/internal/tss/utils/transaction_service_test.go index f58d2fd..8a94f86 100644 --- a/internal/tss/utils/transaction_service_test.go +++ b/internal/tss/utils/transaction_service_test.go @@ -373,7 +373,7 @@ func TestSendRPCRequest(t *testing.T) { assert.Equal(t, "sendTransaction: parsing RPC response JSON", err.Error()) }) - t.Run("returns_rpc_response", func(t *testing.T) { + t.Run("response_has_no_result_field", func(t *testing.T) { httpResponse := &http.Response{ StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(`{"status": "success"}`)), @@ -385,127 +385,108 @@ func TestSendRPCRequest(t *testing.T) { resp, err := txService.sendRPCRequest(method, params) - assert.Equal(t, resp, map[string]interface{}{"status": "success"}) - assert.Empty(t, err) - }) -} - -func TestSendTransaction(t *testing.T) { - mockHTTPClient := MockHTTPClient{} - rpcURL := "http://localhost:8000/soroban/rpc" - txService, _ := NewTransactionService(TransactionServiceOptions{ - DistributionAccountSignatureClient: &signing.SignatureClientMock{}, - ChannelAccountSignatureClient: &signing.SignatureClientMock{}, - HorizonClient: &horizonclient.MockClient{}, - RPCURL: rpcURL, - BaseFee: 114, - HTTPClient: &mockHTTPClient, + assert.Empty(t, resp) + assert.Equal(t, "sendTransaction: response missing result field", err.Error()) }) - method := "sendTransaction" - params := map[string]string{"transaction": "ABCD"} - payload := map[string]interface{}{ - "jsonrpc": "2.0", - "id": 1, - "method": method, - "params": params, - } - jsonData, _ := json.Marshal(payload) - t.Run("rpc_request_fails", func(t *testing.T) { + t.Run("response_has_status_field", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"result": {"status": "PENDING"}}`)), + } mockHTTPClient. On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(&http.Response{}, errors.New("RPC Connection fail")). + Return(httpResponse, nil). Once() - resp, err := txService.SendTransaction("ABCD") - - assert.Equal(t, tss.RPCFailCode, resp.Code.OtherCodes) - assert.Equal(t, "sendTransaction: sending POST request to rpc: RPC Connection fail", err.Error()) + resp, err := txService.sendRPCRequest(method, params) + assert.Equal(t, "PENDING", resp.Status) + assert.Empty(t, err) }) - t.Run("response_has_no_result_field", func(t *testing.T) { + t.Run("response_has_envelopexdr_field", func(t *testing.T) { httpResponse := &http.Response{ StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"foo": "bar"}`)), + Body: io.NopCloser(strings.NewReader(`{"result": {"envelopeXdr": "exdr"}}`)), } mockHTTPClient. On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). Return(httpResponse, nil). Once() - resp, err := txService.SendTransaction("ABCD") - - assert.Equal(t, tss.RPCFailCode, resp.Code.OtherCodes) - assert.Equal(t, "RPC response has no result field", err.Error()) + resp, err := txService.sendRPCRequest(method, params) + assert.Equal(t, "exdr", resp.EnvelopeXDR) + assert.Empty(t, err) }) - t.Run("response_has_status_field", func(t *testing.T) { + t.Run("response_has_resultxdr_field", func(t *testing.T) { httpResponse := &http.Response{ StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"status": "PENDING"}}`)), + Body: io.NopCloser(strings.NewReader(`{"result": {"resultXdr": "rxdr"}}`)), } mockHTTPClient. On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). Return(httpResponse, nil). Once() - resp, err := txService.SendTransaction("ABCD") + resp, err := txService.sendRPCRequest(method, params) - assert.Equal(t, tss.PendingStatus, resp.Status) + assert.Equal(t, "rxdr", resp.ResultXDR) assert.Empty(t, err) }) - t.Run("response_has_hash_field", func(t *testing.T) { + t.Run("response_has_errorresultxdr_field", func(t *testing.T) { httpResponse := &http.Response{ StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"hash": "xyz"}}`)), + Body: io.NopCloser(strings.NewReader(`{"result": {"errorResultXdr": "exdr"}}`)), } mockHTTPClient. On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). Return(httpResponse, nil). Once() - resp, err := txService.SendTransaction("ABCD") + resp, err := txService.sendRPCRequest(method, params) - assert.Equal(t, "xyz", resp.TransactionHash) + assert.Equal(t, "exdr", resp.ErrorResultXDR) assert.Empty(t, err) }) - t.Run("response_has_unparsable_errorResultXdr", func(t *testing.T) { + t.Run("response_has_hash_field", func(t *testing.T) { httpResponse := &http.Response{ StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"errorResultXdr": "ABC123"}}`)), + Body: io.NopCloser(strings.NewReader(`{"result": {"hash": "hash"}}`)), } mockHTTPClient. On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). Return(httpResponse, nil). Once() - resp, err := txService.SendTransaction("ABCD") + resp, err := txService.sendRPCRequest(method, params) - assert.Equal(t, tss.UnMarshalBinaryCode, resp.Code.OtherCodes) - assert.Equal(t, "unable to unmarshal errorResultXdr: ABC123", err.Error()) + assert.Equal(t, "hash", resp.Hash) + assert.Empty(t, err) }) - t.Run("response_has_errorResultXdr", func(t *testing.T) { + + t.Run("response_has_createdat_field", func(t *testing.T) { httpResponse := &http.Response{ StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"errorResultXdr": "AAAAAAAAAMj////9AAAAAA=="}}`)), + Body: io.NopCloser(strings.NewReader(`{"result": {"createdAt": "1234"}}`)), } mockHTTPClient. On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). Return(httpResponse, nil). Once() - resp, err := txService.SendTransaction("ABCD") + resp, err := txService.sendRPCRequest(method, params) - assert.Equal(t, xdr.TransactionResultCodeTxTooLate, resp.Code.TxResultCode) + assert.Equal(t, "1234", resp.CreatedAt) assert.Empty(t, err) }) } -func TestGetTransaction(t *testing.T) { +func TestSendTransaction(t *testing.T) { mockHTTPClient := MockHTTPClient{} rpcURL := "http://localhost:8000/soroban/rpc" txService, _ := NewTransactionService(TransactionServiceOptions{ @@ -516,8 +497,8 @@ func TestGetTransaction(t *testing.T) { BaseFee: 114, HTTPClient: &mockHTTPClient, }) - method := "getTransaction" - params := map[string]string{"hash": "XYZ"} + method := "sendTransaction" + params := map[string]string{"transaction": "ABCD"} payload := map[string]interface{}{ "jsonrpc": "2.0", "id": 1, @@ -532,81 +513,81 @@ func TestGetTransaction(t *testing.T) { Return(&http.Response{}, errors.New("RPC Connection fail")). Once() - resp, err := txService.GetTransaction("XYZ") + resp, err := txService.SendTransaction("ABCD") - assert.Equal(t, tss.ErrorStatus, resp.Status) - assert.Equal(t, "getTransaction: sending POST request to rpc: RPC Connection fail", err.Error()) + assert.Equal(t, tss.RPCFailCode, resp.Code.OtherCodes) + assert.Equal(t, "RPC fail: sendTransaction: sending POST request to rpc: RPC Connection fail", err.Error()) }) - - t.Run("response_has_no_result_field", func(t *testing.T) { + t.Run("response_has_unparsable_errorResultXdr", func(t *testing.T) { httpResponse := &http.Response{ StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"foo": "bar"}`)), + Body: io.NopCloser(strings.NewReader(`{"result": {"errorResultXdr": "ABC123"}}`)), } mockHTTPClient. On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). Return(httpResponse, nil). Once() - resp, err := txService.GetTransaction("XYZ") + resp, err := txService.SendTransaction("ABCD") - assert.Equal(t, tss.ErrorStatus, resp.Status) - assert.Equal(t, "RPC response has no result field", err.Error()) + assert.Equal(t, tss.UnMarshalBinaryCode, resp.Code.OtherCodes) + assert.Equal(t, "unable to unmarshal errorResultXdr: ABC123", err.Error()) }) - - t.Run("response_has_status_field", func(t *testing.T) { + t.Run("response_has_errorResultXdr", func(t *testing.T) { httpResponse := &http.Response{ StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"status": "SUCCESS"}}`)), + Body: io.NopCloser(strings.NewReader(`{"result": {"errorResultXdr": "AAAAAAAAAMj////9AAAAAA=="}}`)), } mockHTTPClient. On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). Return(httpResponse, nil). Once() - resp, err := txService.GetTransaction("XYZ") + resp, err := txService.SendTransaction("ABCD") - assert.Equal(t, tss.SuccessStatus, resp.Status) + assert.Equal(t, xdr.TransactionResultCodeTxTooLate, resp.Code.TxResultCode) assert.Empty(t, err) }) +} - t.Run("response_has_envelopeXdr_field", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"envelopeXdr": "envelopeABCD"}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.GetTransaction("XYZ") - - assert.Equal(t, "envelopeABCD", resp.EnvelopeXDR) - assert.Empty(t, err) +func TestGetTransaction(t *testing.T) { + mockHTTPClient := MockHTTPClient{} + rpcURL := "http://localhost:8000/soroban/rpc" + txService, _ := NewTransactionService(TransactionServiceOptions{ + DistributionAccountSignatureClient: &signing.SignatureClientMock{}, + ChannelAccountSignatureClient: &signing.SignatureClientMock{}, + HorizonClient: &horizonclient.MockClient{}, + RPCURL: rpcURL, + BaseFee: 114, + HTTPClient: &mockHTTPClient, }) + method := "getTransaction" + params := map[string]string{"hash": "XYZ"} + payload := map[string]interface{}{ + "jsonrpc": "2.0", + "id": 1, + "method": method, + "params": params, + } + jsonData, _ := json.Marshal(payload) - t.Run("response_has_resultXdr_field", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"resultXdr": "resultABCD"}}`)), - } + t.Run("rpc_request_fails", func(t *testing.T) { mockHTTPClient. On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). + Return(&http.Response{}, errors.New("RPC Connection fail")). Once() resp, err := txService.GetTransaction("XYZ") - assert.Equal(t, "resultABCD", resp.ResultXDR) - assert.Empty(t, err) - }) + assert.Equal(t, tss.ErrorStatus, resp.Status) + assert.Equal(t, "RPC Fail: getTransaction: sending POST request to rpc: RPC Connection fail", err.Error()) + }) t.Run("unable_to_parse_createdAt", func(t *testing.T) { httpResponse := &http.Response{ StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"createdAt": "ABCD"}}`)), + Body: io.NopCloser(strings.NewReader(`{"result": {"status": "SUCCESS", "createdAt": "ABCD"}}`)), } mockHTTPClient. On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). @@ -616,9 +597,8 @@ func TestGetTransaction(t *testing.T) { resp, err := txService.GetTransaction("XYZ") assert.Equal(t, tss.ErrorStatus, resp.Status) - assert.Equal(t, "cannot parse createdAt", err.Error()) + assert.Equal(t, "unable to parse createAt: strconv.ParseInt: parsing \"ABCD\": invalid syntax", err.Error()) }) - t.Run("response_has_createdAt_field", func(t *testing.T) { httpResponse := &http.Response{ StatusCode: http.StatusOK, From 1a3c17cdf61d04b13b994c92caa0c05508da0a84 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Thu, 19 Sep 2024 20:22:22 -0700 Subject: [PATCH 043/113] remove comment --- internal/tss/utils/transaction_builder.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/tss/utils/transaction_builder.go b/internal/tss/utils/transaction_builder.go index 38a3190..a6dc192 100644 --- a/internal/tss/utils/transaction_builder.go +++ b/internal/tss/utils/transaction_builder.go @@ -41,7 +41,6 @@ func BuildOriginalTransaction(txOpXDRs []string) (*txnbuild.Transaction, error) SourceAccount: &txnbuild.SimpleAccount{ AccountID: keypair.MustRandom().Address(), }, - //IncrementSequenceNum: true, Operations: operations, BaseFee: 104, Preconditions: txnbuild.Preconditions{TimeBounds: txnbuild.NewTimeout(10)}, From 174ed45ca9c6a12ff37ccdc1903ce05695c95d47 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Thu, 19 Sep 2024 20:23:01 -0700 Subject: [PATCH 044/113] remove comment --- internal/tss/utils/transaction_builder.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/tss/utils/transaction_builder.go b/internal/tss/utils/transaction_builder.go index 38a3190..a6dc192 100644 --- a/internal/tss/utils/transaction_builder.go +++ b/internal/tss/utils/transaction_builder.go @@ -41,7 +41,6 @@ func BuildOriginalTransaction(txOpXDRs []string) (*txnbuild.Transaction, error) SourceAccount: &txnbuild.SimpleAccount{ AccountID: keypair.MustRandom().Address(), }, - //IncrementSequenceNum: true, Operations: operations, BaseFee: 104, Preconditions: txnbuild.Preconditions{TimeBounds: txnbuild.NewTimeout(10)}, From 9492d7159b30d3d7e25a3caac9365c08d3c8da28 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Thu, 19 Sep 2024 20:31:39 -0700 Subject: [PATCH 045/113] latest tx service changes --- internal/tss/types.go | 13 ++ internal/tss/utils/transaction_builder.go | 14 +- internal/tss/utils/transaction_service.go | 137 ++++++-------- .../tss/utils/transaction_service_test.go | 174 ++++++++---------- 4 files changed, 148 insertions(+), 190 deletions(-) diff --git a/internal/tss/types.go b/internal/tss/types.go index 85202d9..0489bf6 100644 --- a/internal/tss/types.go +++ b/internal/tss/types.go @@ -80,6 +80,19 @@ type Payload struct { RpcGetIngestTxResponse RPCGetIngestTxResponse } +type RPCResult struct { + Status string `json:"status"` + EnvelopeXDR string `json:"envelopeXdr"` + ResultXDR string `json:"resultXdr"` + ErrorResultXDR string `json:"errorResultXdr"` + Hash string `json:"hash"` + CreatedAt string `json:"createdAt"` +} + +type RPCResponse struct { + RPCResult `json:"result"` +} + type Channel interface { Send(payload Payload) Receive(payload Payload) diff --git a/internal/tss/utils/transaction_builder.go b/internal/tss/utils/transaction_builder.go index 8347200..a6dc192 100644 --- a/internal/tss/utils/transaction_builder.go +++ b/internal/tss/utils/transaction_builder.go @@ -1,9 +1,9 @@ package utils import ( + "bytes" "encoding/base64" "fmt" - "strings" xdr3 "github.com/stellar/go-xdr/xdr3" "github.com/stellar/go/keypair" @@ -18,9 +18,9 @@ func BuildOriginalTransaction(txOpXDRs []string) (*txnbuild.Transaction, error) if err != nil { return nil, fmt.Errorf("decoding Operation XDR string") } - dec := xdr3.NewDecoder(strings.NewReader(string(decodedBytes))) var decodedOp xdr.Operation - _, err = dec.Decode(&decodedOp) + _, err = xdr3.Unmarshal(bytes.NewReader(decodedBytes), &decodedOp) + if err != nil { return nil, fmt.Errorf("decoding xdr into xdr Operation: %w", err) } @@ -40,12 +40,10 @@ func BuildOriginalTransaction(txOpXDRs []string) (*txnbuild.Transaction, error) tx, _ := txnbuild.NewTransaction(txnbuild.TransactionParams{ SourceAccount: &txnbuild.SimpleAccount{ AccountID: keypair.MustRandom().Address(), - Sequence: 123, }, - IncrementSequenceNum: true, - Operations: operations, - BaseFee: 104, - Preconditions: txnbuild.Preconditions{TimeBounds: txnbuild.NewTimeout(10)}, + Operations: operations, + BaseFee: 104, + Preconditions: txnbuild.Preconditions{TimeBounds: txnbuild.NewTimeout(10)}, }) return tx, nil } diff --git a/internal/tss/utils/transaction_service.go b/internal/tss/utils/transaction_service.go index 35f89a0..83e5643 100644 --- a/internal/tss/utils/transaction_service.go +++ b/internal/tss/utils/transaction_service.go @@ -9,7 +9,6 @@ import ( "io" "net/http" "strconv" - "strings" xdr3 "github.com/stellar/go-xdr/xdr3" "github.com/stellar/go/clients/horizonclient" @@ -38,7 +37,6 @@ type transactionService struct { RPCURL string BaseFee int64 HTTPClient HTTPClient - Ctx context.Context } var _ TransactionService = (*transactionService)(nil) @@ -94,37 +92,6 @@ func NewTransactionService(opts TransactionServiceOptions) (*transactionService, }, nil } -func (t *transactionService) sendRPCRequest(method string, params map[string]string) (map[string]interface{}, error) { - payload := map[string]interface{}{ - "jsonrpc": "2.0", - "id": 1, - "method": method, - "params": params, - } - jsonData, err := json.Marshal(payload) - - if err != nil { - return nil, fmt.Errorf("marshaling payload") - } - - resp, err := t.HTTPClient.Post(t.RPCURL, "application/json", bytes.NewBuffer(jsonData)) - if err != nil { - return nil, fmt.Errorf("%s: sending POST request to rpc: %v", method, err) - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return nil, fmt.Errorf("%s: unmarshaling RPC response", method) - } - var res map[string]interface{} - err = json.Unmarshal(body, &res) - if err != nil { - return nil, fmt.Errorf("%s: parsing RPC response JSON", method) - } - return res, nil -} - func (t *transactionService) NetworkPassphrase() string { return t.DistributionAccountSignatureClient.NetworkPassphrase() } @@ -152,7 +119,7 @@ func (t *transactionService) SignAndBuildNewFeeBumpTransaction(ctx context.Conte Operations: originalTx.Operations(), BaseFee: int64(t.BaseFee), Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewTimeout(10), + TimeBounds: txnbuild.NewTimeout(300), }, IncrementSequenceNum: true, }, @@ -189,83 +156,83 @@ func (t *transactionService) SignAndBuildNewFeeBumpTransaction(ctx context.Conte } func (t *transactionService) parseErrorResultXDR(errorResultXdr string) (tss.RPCTXCode, error) { - - //errorResult := xdr.TransactionResult{} - unMarshallErr := "unable to unmarshal errorResultXdr: %s" - //err := errorResult.UnmarshalBinary([]byte(errorResultXdr)) - + unMarshalErr := "unable to unmarshal errorResultXdr: %s" decodedBytes, err := base64.StdEncoding.DecodeString(errorResultXdr) if err != nil { - return tss.RPCTXCode{OtherCodes: tss.UnMarshalBinaryCode}, fmt.Errorf(unMarshallErr, errorResultXdr) + return tss.RPCTXCode{OtherCodes: tss.UnMarshalBinaryCode}, fmt.Errorf(unMarshalErr, errorResultXdr) } - dec := xdr3.NewDecoder(strings.NewReader(string(decodedBytes))) var errorResult xdr.TransactionResult - _, err = dec.Decode(&errorResult) - + _, err = xdr3.Unmarshal(bytes.NewReader(decodedBytes), &errorResult) if err != nil { - return tss.RPCTXCode{OtherCodes: tss.UnMarshalBinaryCode}, fmt.Errorf(unMarshallErr, errorResultXdr) + return tss.RPCTXCode{OtherCodes: tss.UnMarshalBinaryCode}, fmt.Errorf(unMarshalErr, errorResultXdr) } return tss.RPCTXCode{ TxResultCode: errorResult.Result.Code, }, nil } +func (t *transactionService) sendRPCRequest(method string, params map[string]string) (tss.RPCResponse, error) { + payload := map[string]interface{}{ + "jsonrpc": "2.0", + "id": 1, + "method": method, + "params": params, + } + jsonData, err := json.Marshal(payload) + + if err != nil { + return tss.RPCResponse{}, fmt.Errorf("marshaling payload") + } + + resp, err := t.HTTPClient.Post(t.RPCURL, "application/json", bytes.NewBuffer(jsonData)) + if err != nil { + return tss.RPCResponse{}, fmt.Errorf("%s: sending POST request to rpc: %v", method, err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return tss.RPCResponse{}, fmt.Errorf("%s: unmarshaling RPC response", method) + } + var res tss.RPCResponse + err = json.Unmarshal(body, &res) + if err != nil { + return tss.RPCResponse{}, fmt.Errorf("%s: parsing RPC response JSON", method) + } + if res.RPCResult == (tss.RPCResult{}) { + return tss.RPCResponse{}, fmt.Errorf("%s: response missing result field", method) + } + return res, nil +} + func (t *transactionService) SendTransaction(transactionXdr string) (tss.RPCSendTxResponse, error) { rpcResponse, err := t.sendRPCRequest("sendTransaction", map[string]string{"transaction": transactionXdr}) sendTxResponse := tss.RPCSendTxResponse{} sendTxResponse.TransactionXDR = transactionXdr if err != nil { sendTxResponse.Code.OtherCodes = tss.RPCFailCode - return sendTxResponse, fmt.Errorf(err.Error()) - } - - if result, ok := rpcResponse["result"].(map[string]interface{}); ok { - if val, exists := result["status"].(string); exists { - sendTxResponse.Status = tss.RPCTXStatus(val) - } - if val, exists := result["errorResultXdr"].(string); exists { - sendTxResponse.Code, err = t.parseErrorResultXDR(val) - } - if hash, exists := result["hash"].(string); exists { - sendTxResponse.TransactionHash = hash - } - } else { - sendTxResponse.Code.OtherCodes = tss.RPCFailCode - return sendTxResponse, fmt.Errorf("RPC response has no result field") + return sendTxResponse, fmt.Errorf("RPC fail: %s", err.Error()) } + sendTxResponse.Status = tss.RPCTXStatus(rpcResponse.RPCResult.Status) + sendTxResponse.Code, err = t.parseErrorResultXDR(rpcResponse.RPCResult.ErrorResultXDR) + sendTxResponse.TransactionHash = rpcResponse.RPCResult.Hash return sendTxResponse, err } func (t *transactionService) GetTransaction(transactionHash string) (tss.RPCGetIngestTxResponse, error) { rpcResponse, err := t.sendRPCRequest("getTransaction", map[string]string{"hash": transactionHash}) if err != nil { - return tss.RPCGetIngestTxResponse{Status: tss.ErrorStatus}, fmt.Errorf(err.Error()) + return tss.RPCGetIngestTxResponse{Status: tss.ErrorStatus}, fmt.Errorf("RPC Fail: %s", err.Error()) } - getIngestTxResponse := tss.RPCGetIngestTxResponse{} - if result, ok := rpcResponse["result"].(map[string]interface{}); ok { - if status, exists := result["status"].(string); exists { - getIngestTxResponse.Status = tss.RPCTXStatus(status) + getIngestTxResponse.Status = tss.RPCTXStatus(rpcResponse.RPCResult.Status) + getIngestTxResponse.EnvelopeXDR = rpcResponse.RPCResult.EnvelopeXDR + getIngestTxResponse.ResultXDR = rpcResponse.RPCResult.ResultXDR + if getIngestTxResponse.Status != tss.NotFoundStatus { + getIngestTxResponse.CreatedAt, err = strconv.ParseInt(rpcResponse.RPCResult.CreatedAt, 10, 64) + if err != nil { + return tss.RPCGetIngestTxResponse{Status: tss.ErrorStatus}, fmt.Errorf("unable to parse createAt: %s", err.Error()) } - if envelopeXDR, exists := result["envelopeXdr"].(string); exists { - getIngestTxResponse.EnvelopeXDR = envelopeXDR - } - if resultXDR, exists := result["resultXdr"].(string); exists { - getIngestTxResponse.ResultXDR = resultXDR - } - if createdAt, exists := result["createdAt"].(string); exists { - createdAtInt, e := strconv.ParseInt(createdAt, 10, 64) - if e != nil { - getIngestTxResponse.Status = tss.ErrorStatus - err = fmt.Errorf("cannot parse createdAt") - } else { - getIngestTxResponse.CreatedAt = createdAtInt - } - } - } else { - getIngestTxResponse.Status = tss.ErrorStatus - return getIngestTxResponse, fmt.Errorf("RPC response has no result field") - } - return getIngestTxResponse, err + return getIngestTxResponse, nil } diff --git a/internal/tss/utils/transaction_service_test.go b/internal/tss/utils/transaction_service_test.go index f58d2fd..8a94f86 100644 --- a/internal/tss/utils/transaction_service_test.go +++ b/internal/tss/utils/transaction_service_test.go @@ -373,7 +373,7 @@ func TestSendRPCRequest(t *testing.T) { assert.Equal(t, "sendTransaction: parsing RPC response JSON", err.Error()) }) - t.Run("returns_rpc_response", func(t *testing.T) { + t.Run("response_has_no_result_field", func(t *testing.T) { httpResponse := &http.Response{ StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(`{"status": "success"}`)), @@ -385,127 +385,108 @@ func TestSendRPCRequest(t *testing.T) { resp, err := txService.sendRPCRequest(method, params) - assert.Equal(t, resp, map[string]interface{}{"status": "success"}) - assert.Empty(t, err) - }) -} - -func TestSendTransaction(t *testing.T) { - mockHTTPClient := MockHTTPClient{} - rpcURL := "http://localhost:8000/soroban/rpc" - txService, _ := NewTransactionService(TransactionServiceOptions{ - DistributionAccountSignatureClient: &signing.SignatureClientMock{}, - ChannelAccountSignatureClient: &signing.SignatureClientMock{}, - HorizonClient: &horizonclient.MockClient{}, - RPCURL: rpcURL, - BaseFee: 114, - HTTPClient: &mockHTTPClient, + assert.Empty(t, resp) + assert.Equal(t, "sendTransaction: response missing result field", err.Error()) }) - method := "sendTransaction" - params := map[string]string{"transaction": "ABCD"} - payload := map[string]interface{}{ - "jsonrpc": "2.0", - "id": 1, - "method": method, - "params": params, - } - jsonData, _ := json.Marshal(payload) - t.Run("rpc_request_fails", func(t *testing.T) { + t.Run("response_has_status_field", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"result": {"status": "PENDING"}}`)), + } mockHTTPClient. On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(&http.Response{}, errors.New("RPC Connection fail")). + Return(httpResponse, nil). Once() - resp, err := txService.SendTransaction("ABCD") - - assert.Equal(t, tss.RPCFailCode, resp.Code.OtherCodes) - assert.Equal(t, "sendTransaction: sending POST request to rpc: RPC Connection fail", err.Error()) + resp, err := txService.sendRPCRequest(method, params) + assert.Equal(t, "PENDING", resp.Status) + assert.Empty(t, err) }) - t.Run("response_has_no_result_field", func(t *testing.T) { + t.Run("response_has_envelopexdr_field", func(t *testing.T) { httpResponse := &http.Response{ StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"foo": "bar"}`)), + Body: io.NopCloser(strings.NewReader(`{"result": {"envelopeXdr": "exdr"}}`)), } mockHTTPClient. On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). Return(httpResponse, nil). Once() - resp, err := txService.SendTransaction("ABCD") - - assert.Equal(t, tss.RPCFailCode, resp.Code.OtherCodes) - assert.Equal(t, "RPC response has no result field", err.Error()) + resp, err := txService.sendRPCRequest(method, params) + assert.Equal(t, "exdr", resp.EnvelopeXDR) + assert.Empty(t, err) }) - t.Run("response_has_status_field", func(t *testing.T) { + t.Run("response_has_resultxdr_field", func(t *testing.T) { httpResponse := &http.Response{ StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"status": "PENDING"}}`)), + Body: io.NopCloser(strings.NewReader(`{"result": {"resultXdr": "rxdr"}}`)), } mockHTTPClient. On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). Return(httpResponse, nil). Once() - resp, err := txService.SendTransaction("ABCD") + resp, err := txService.sendRPCRequest(method, params) - assert.Equal(t, tss.PendingStatus, resp.Status) + assert.Equal(t, "rxdr", resp.ResultXDR) assert.Empty(t, err) }) - t.Run("response_has_hash_field", func(t *testing.T) { + t.Run("response_has_errorresultxdr_field", func(t *testing.T) { httpResponse := &http.Response{ StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"hash": "xyz"}}`)), + Body: io.NopCloser(strings.NewReader(`{"result": {"errorResultXdr": "exdr"}}`)), } mockHTTPClient. On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). Return(httpResponse, nil). Once() - resp, err := txService.SendTransaction("ABCD") + resp, err := txService.sendRPCRequest(method, params) - assert.Equal(t, "xyz", resp.TransactionHash) + assert.Equal(t, "exdr", resp.ErrorResultXDR) assert.Empty(t, err) }) - t.Run("response_has_unparsable_errorResultXdr", func(t *testing.T) { + t.Run("response_has_hash_field", func(t *testing.T) { httpResponse := &http.Response{ StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"errorResultXdr": "ABC123"}}`)), + Body: io.NopCloser(strings.NewReader(`{"result": {"hash": "hash"}}`)), } mockHTTPClient. On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). Return(httpResponse, nil). Once() - resp, err := txService.SendTransaction("ABCD") + resp, err := txService.sendRPCRequest(method, params) - assert.Equal(t, tss.UnMarshalBinaryCode, resp.Code.OtherCodes) - assert.Equal(t, "unable to unmarshal errorResultXdr: ABC123", err.Error()) + assert.Equal(t, "hash", resp.Hash) + assert.Empty(t, err) }) - t.Run("response_has_errorResultXdr", func(t *testing.T) { + + t.Run("response_has_createdat_field", func(t *testing.T) { httpResponse := &http.Response{ StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"errorResultXdr": "AAAAAAAAAMj////9AAAAAA=="}}`)), + Body: io.NopCloser(strings.NewReader(`{"result": {"createdAt": "1234"}}`)), } mockHTTPClient. On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). Return(httpResponse, nil). Once() - resp, err := txService.SendTransaction("ABCD") + resp, err := txService.sendRPCRequest(method, params) - assert.Equal(t, xdr.TransactionResultCodeTxTooLate, resp.Code.TxResultCode) + assert.Equal(t, "1234", resp.CreatedAt) assert.Empty(t, err) }) } -func TestGetTransaction(t *testing.T) { +func TestSendTransaction(t *testing.T) { mockHTTPClient := MockHTTPClient{} rpcURL := "http://localhost:8000/soroban/rpc" txService, _ := NewTransactionService(TransactionServiceOptions{ @@ -516,8 +497,8 @@ func TestGetTransaction(t *testing.T) { BaseFee: 114, HTTPClient: &mockHTTPClient, }) - method := "getTransaction" - params := map[string]string{"hash": "XYZ"} + method := "sendTransaction" + params := map[string]string{"transaction": "ABCD"} payload := map[string]interface{}{ "jsonrpc": "2.0", "id": 1, @@ -532,81 +513,81 @@ func TestGetTransaction(t *testing.T) { Return(&http.Response{}, errors.New("RPC Connection fail")). Once() - resp, err := txService.GetTransaction("XYZ") + resp, err := txService.SendTransaction("ABCD") - assert.Equal(t, tss.ErrorStatus, resp.Status) - assert.Equal(t, "getTransaction: sending POST request to rpc: RPC Connection fail", err.Error()) + assert.Equal(t, tss.RPCFailCode, resp.Code.OtherCodes) + assert.Equal(t, "RPC fail: sendTransaction: sending POST request to rpc: RPC Connection fail", err.Error()) }) - - t.Run("response_has_no_result_field", func(t *testing.T) { + t.Run("response_has_unparsable_errorResultXdr", func(t *testing.T) { httpResponse := &http.Response{ StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"foo": "bar"}`)), + Body: io.NopCloser(strings.NewReader(`{"result": {"errorResultXdr": "ABC123"}}`)), } mockHTTPClient. On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). Return(httpResponse, nil). Once() - resp, err := txService.GetTransaction("XYZ") + resp, err := txService.SendTransaction("ABCD") - assert.Equal(t, tss.ErrorStatus, resp.Status) - assert.Equal(t, "RPC response has no result field", err.Error()) + assert.Equal(t, tss.UnMarshalBinaryCode, resp.Code.OtherCodes) + assert.Equal(t, "unable to unmarshal errorResultXdr: ABC123", err.Error()) }) - - t.Run("response_has_status_field", func(t *testing.T) { + t.Run("response_has_errorResultXdr", func(t *testing.T) { httpResponse := &http.Response{ StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"status": "SUCCESS"}}`)), + Body: io.NopCloser(strings.NewReader(`{"result": {"errorResultXdr": "AAAAAAAAAMj////9AAAAAA=="}}`)), } mockHTTPClient. On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). Return(httpResponse, nil). Once() - resp, err := txService.GetTransaction("XYZ") + resp, err := txService.SendTransaction("ABCD") - assert.Equal(t, tss.SuccessStatus, resp.Status) + assert.Equal(t, xdr.TransactionResultCodeTxTooLate, resp.Code.TxResultCode) assert.Empty(t, err) }) +} - t.Run("response_has_envelopeXdr_field", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"envelopeXdr": "envelopeABCD"}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.GetTransaction("XYZ") - - assert.Equal(t, "envelopeABCD", resp.EnvelopeXDR) - assert.Empty(t, err) +func TestGetTransaction(t *testing.T) { + mockHTTPClient := MockHTTPClient{} + rpcURL := "http://localhost:8000/soroban/rpc" + txService, _ := NewTransactionService(TransactionServiceOptions{ + DistributionAccountSignatureClient: &signing.SignatureClientMock{}, + ChannelAccountSignatureClient: &signing.SignatureClientMock{}, + HorizonClient: &horizonclient.MockClient{}, + RPCURL: rpcURL, + BaseFee: 114, + HTTPClient: &mockHTTPClient, }) + method := "getTransaction" + params := map[string]string{"hash": "XYZ"} + payload := map[string]interface{}{ + "jsonrpc": "2.0", + "id": 1, + "method": method, + "params": params, + } + jsonData, _ := json.Marshal(payload) - t.Run("response_has_resultXdr_field", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"resultXdr": "resultABCD"}}`)), - } + t.Run("rpc_request_fails", func(t *testing.T) { mockHTTPClient. On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). + Return(&http.Response{}, errors.New("RPC Connection fail")). Once() resp, err := txService.GetTransaction("XYZ") - assert.Equal(t, "resultABCD", resp.ResultXDR) - assert.Empty(t, err) - }) + assert.Equal(t, tss.ErrorStatus, resp.Status) + assert.Equal(t, "RPC Fail: getTransaction: sending POST request to rpc: RPC Connection fail", err.Error()) + }) t.Run("unable_to_parse_createdAt", func(t *testing.T) { httpResponse := &http.Response{ StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"createdAt": "ABCD"}}`)), + Body: io.NopCloser(strings.NewReader(`{"result": {"status": "SUCCESS", "createdAt": "ABCD"}}`)), } mockHTTPClient. On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). @@ -616,9 +597,8 @@ func TestGetTransaction(t *testing.T) { resp, err := txService.GetTransaction("XYZ") assert.Equal(t, tss.ErrorStatus, resp.Status) - assert.Equal(t, "cannot parse createdAt", err.Error()) + assert.Equal(t, "unable to parse createAt: strconv.ParseInt: parsing \"ABCD\": invalid syntax", err.Error()) }) - t.Run("response_has_createdAt_field", func(t *testing.T) { httpResponse := &http.Response{ StatusCode: http.StatusOK, From 5d31b01e1f4eec1c8a6c66359213976953688b8f Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Thu, 19 Sep 2024 21:33:12 -0700 Subject: [PATCH 046/113] adding a router + utils file --- go.mod | 2 +- internal/serve/serve.go | 18 ++- .../channels/rpc_caller_service_channel.go | 92 ++++------- .../rpc_caller_service_channel_test.go | 63 ++++---- internal/tss/channels/utils.go | 46 ++++++ internal/tss/channels/utils_test.go | 143 ++++++++++++++++++ internal/tss/router/mocks.go | 16 ++ internal/tss/router/router.go | 65 ++++++++ internal/tss/router/router_test.go | 51 +++++++ .../tss/services/webhook_handler_service.go | 19 +++ 10 files changed, 418 insertions(+), 97 deletions(-) create mode 100644 internal/tss/channels/utils.go create mode 100644 internal/tss/channels/utils_test.go create mode 100644 internal/tss/router/mocks.go create mode 100644 internal/tss/router/router.go create mode 100644 internal/tss/router/router_test.go create mode 100644 internal/tss/services/webhook_handler_service.go diff --git a/go.mod b/go.mod index 063511e..3ddd30a 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( github.com/stellar/go v0.0.0-20240416222646-fd107948e6c4 github.com/stellar/go-xdr v0.0.0-20231122183749-b53fb00bcac2 github.com/stretchr/testify v1.9.0 + golang.org/x/net v0.23.0 golang.org/x/term v0.18.0 ) @@ -90,7 +91,6 @@ require ( golang.org/x/crypto v0.21.0 // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect golang.org/x/mod v0.13.0 // indirect - golang.org/x/net v0.23.0 // indirect golang.org/x/oauth2 v0.16.0 // indirect golang.org/x/sync v0.6.0 // indirect golang.org/x/sys v0.18.0 // indirect diff --git a/internal/serve/serve.go b/internal/serve/serve.go index 7261eb5..4f448b3 100644 --- a/internal/serve/serve.go +++ b/internal/serve/serve.go @@ -27,6 +27,7 @@ import ( signingutils "github.com/stellar/wallet-backend/internal/signing/utils" "github.com/stellar/wallet-backend/internal/tss" tsschannel "github.com/stellar/wallet-backend/internal/tss/channels" + tssrouter "github.com/stellar/wallet-backend/internal/tss/router" tssservices "github.com/stellar/wallet-backend/internal/tss/services" tssstore "github.com/stellar/wallet-backend/internal/tss/store" tssutils "github.com/stellar/wallet-backend/internal/tss/utils" @@ -168,12 +169,14 @@ func initHandlerDeps(cfg Configs) (handlerDeps, error) { go ensureChannelAccounts(channelAccountService, int64(cfg.NumberOfChannelAccounts)) // TSS + httpClient := http.Client{Timeout: time.Duration(30 * time.Second)} txServiceOpts := tssutils.TransactionServiceOptions{ DistributionAccountSignatureClient: cfg.DistributionAccountSignatureClient, ChannelAccountSignatureClient: cfg.ChannelAccountSignatureClient, HorizonClient: &horizonClient, RPCURL: cfg.RpcUrl, BaseFee: int64(cfg.BaseFee), // Reuse horizon base fee for RPC?? + HTTPClient: &httpClient, } tssTxService, err := tssutils.NewTransactionService(txServiceOpts) if err != nil { @@ -183,12 +186,17 @@ func initHandlerDeps(cfg Configs) (handlerDeps, error) { // re-use same context as above?? store := tssstore.NewStore(dbConnectionPool) errorHandlerService := tssservices.NewErrorHandlerService(nil) + webhookHandlerService := tssservices.NewWebhookHandlerService(nil) + router := tssrouter.NewRouter(tssrouter.RouterConfigs{ + ErrorHandlerService: errorHandlerService, + WebhookHandlerService: webhookHandlerService, + }) tssChannelConfigs := tsschannel.RPCCallerServiceChannelConfigs{ - Store: store, - TxService: tssTxService, - ErrHandlerService: errorHandlerService, - MaxBufferSize: cfg.RPCCallerServiceChannelBufferSize, - MaxWorkers: cfg.RPCCallerServiceChannelMaxWorkers, + Store: store, + TxService: tssTxService, + Router: router, + MaxBufferSize: cfg.RPCCallerServiceChannelBufferSize, + MaxWorkers: cfg.RPCCallerServiceChannelMaxWorkers, } rpcCallerServiceChannel := tsschannel.NewRPCCallerServiceChannel(tssChannelConfigs) rpcCallerService := tssservices.NewRPCCallerService(rpcCallerServiceChannel) diff --git a/internal/tss/channels/rpc_caller_service_channel.go b/internal/tss/channels/rpc_caller_service_channel.go index e75b899..9cb4f47 100644 --- a/internal/tss/channels/rpc_caller_service_channel.go +++ b/internal/tss/channels/rpc_caller_service_channel.go @@ -4,40 +4,44 @@ import ( "context" "github.com/alitto/pond" + "github.com/stellar/go/support/log" "github.com/stellar/wallet-backend/internal/tss" - tss_services "github.com/stellar/wallet-backend/internal/tss/services" - tss_store "github.com/stellar/wallet-backend/internal/tss/store" + "github.com/stellar/wallet-backend/internal/tss/router" + "github.com/stellar/wallet-backend/internal/tss/services" + "github.com/stellar/wallet-backend/internal/tss/store" "github.com/stellar/wallet-backend/internal/tss/utils" ) type RPCCallerServiceChannelConfigs struct { - Store tss_store.Store - TxService utils.TransactionService - ErrHandlerService tss_services.Service - MaxBufferSize int - MaxWorkers int + Store store.Store + TxService utils.TransactionService + Router router.Router + MaxBufferSize int + MaxWorkers int } type rpcCallerServicePool struct { - pool *pond.WorkerPool - txService utils.TransactionService - errHandlerService tss_services.Service - store tss_store.Store + Pool *pond.WorkerPool + TxService utils.TransactionService + ErrHandlerService services.Service + Store store.Store + Router router.Router } func NewRPCCallerServiceChannel(cfg RPCCallerServiceChannelConfigs) tss.Channel { pool := pond.New(cfg.MaxBufferSize, cfg.MaxWorkers, pond.Strategy(pond.Balanced())) return &rpcCallerServicePool{ - pool: pool, - txService: cfg.TxService, - errHandlerService: cfg.ErrHandlerService, - store: cfg.Store, + Pool: pool, + TxService: cfg.TxService, + Store: cfg.Store, + Router: cfg.Router, } + } func (p *rpcCallerServicePool) Send(payload tss.Payload) { - p.pool.Submit(func() { + p.Pool.Submit(func() { p.Receive(payload) }) } @@ -45,64 +49,30 @@ func (p *rpcCallerServicePool) Send(payload tss.Payload) { func (p *rpcCallerServicePool) Receive(payload tss.Payload) { ctx := context.Background() + // Create a new transaction record in the transactions table. + err := p.Store.UpsertTransaction(ctx, payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.NewStatus) - err := p.store.UpsertTransaction(ctx, payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.NewStatus) - if err != nil { - log.Errorf("RPCCallerService: Unable to upsert transaction into transactions table: %s", err.Error()) - return - } - /* - The reason we return on each error we encounter is so that the transaction status - stays at NEW, so that it does not progress any further and - can be picked up for re-processing when this pool is restarted. - */ - feeBumpTx, err := p.txService.SignAndBuildNewFeeBumpTransaction(ctx, payload.TransactionXDR) if err != nil { - log.Errorf("RPCCallerService: Unable to sign/build transaction: %s", err.Error()) - return - } - feeBumpTxHash, err := feeBumpTx.HashHex(p.txService.NetworkPassphrase()) - if err != nil { - log.Errorf("RPCCallerService: Unable to hashhex fee bump transaction: %s", err.Error()) + log.Errorf("Unable to upsert transaction into transactions table: %s", err.Error()) return } - feeBumpTxXDR, err := feeBumpTx.Base64() - if err != nil { - log.Errorf("RPCCallerService: Unable to base64 fee bump transaction: %s", err.Error()) - return - } - err = p.store.UpsertTry(ctx, payload.TransactionHash, feeBumpTxHash, feeBumpTxXDR, tss.RPCTXCode{OtherCodes: tss.NewCode}) - if err != nil { - log.Errorf("RPCCallerService: Unable to upsert try in tries table: %s", err.Error()) - return - } - rpcSendResp, rpcErr := p.txService.SendTransaction(feeBumpTxXDR) - - err = p.store.UpsertTry(ctx, payload.TransactionHash, feeBumpTxHash, feeBumpTxXDR, rpcSendResp.Code) - if err != nil { - log.Errorf("RPCCallerService: Unable to upsert try in tries table: %s", err.Error()) - return - } - // if the rpc submitTransaction fails, or we cannot unmarshal it's response, we return because we want to retry this transaction - if rpcErr != nil && rpcSendResp.Code.OtherCodes == tss.RPCFailCode || rpcSendResp.Code.OtherCodes == tss.UnMarshalBinaryCode { - log.Errorf("RPCCallerService: RPC fail: %s", rpcErr.Error()) - return - } + rpcSendResp, err := BuildAndSubmitTransaction(ctx, "RPCCallerServiceChannel", payload, p.Store, p.TxService) - err = p.store.UpsertTransaction(ctx, payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, rpcSendResp.Status) if err != nil { - log.Errorf("RPCCallerService:Unable to do the final update of tx in the transactions table: %s", err.Error()) + log.Errorf(": Unable to sign and submit transaction: %s", err.Error()) return } - - // route the payload to the Error handler service payload.RpcSubmitTxResponse = rpcSendResp if rpcSendResp.Status == tss.TryAgainLaterStatus || rpcSendResp.Status == tss.ErrorStatus { - p.errHandlerService.ProcessPayload(payload) + p.Router.Route(payload) } } +func (p *rpcCallerServicePool) SetRouter(router router.Router) { + p.Router = router +} + func (p *rpcCallerServicePool) Stop() { - p.pool.StopAndWait() + p.Pool.StopAndWait() } diff --git a/internal/tss/channels/rpc_caller_service_channel_test.go b/internal/tss/channels/rpc_caller_service_channel_test.go index 636412e..d9abc5e 100644 --- a/internal/tss/channels/rpc_caller_service_channel_test.go +++ b/internal/tss/channels/rpc_caller_service_channel_test.go @@ -9,7 +9,7 @@ import ( "github.com/stellar/wallet-backend/internal/db" "github.com/stellar/wallet-backend/internal/db/dbtest" "github.com/stellar/wallet-backend/internal/tss" - tss_services "github.com/stellar/wallet-backend/internal/tss/services" + "github.com/stellar/wallet-backend/internal/tss/router" "github.com/stellar/wallet-backend/internal/tss/store" "github.com/stellar/wallet-backend/internal/tss/utils" "github.com/stretchr/testify/assert" @@ -57,17 +57,21 @@ func TestReceive(t *testing.T) { defer dbConnectionPool.Close() store := store.NewStore(dbConnectionPool) txServiceMock := utils.TransactionServiceMock{} - errHandlerService := tss_services.MockService{} + defer txServiceMock.AssertExpectations(t) + routerMock := router.MockRouter{} + defer routerMock.AssertExpectations(t) cfgs := RPCCallerServiceChannelConfigs{ - Store: store, - TxService: &txServiceMock, - ErrHandlerService: &errHandlerService, - MaxBufferSize: 1, - MaxWorkers: 1, + Store: store, + TxService: &txServiceMock, + Router: &routerMock, + MaxBufferSize: 1, + MaxWorkers: 1, } + networkPass := "passphrase" channel := NewRPCCallerServiceChannel(cfgs) feeBumpTx := utils.BuildTestFeeBumpTransaction() - networkPass := "passphrase" + feeBumpTxXDR, _ := feeBumpTx.Base64() + feeBumpTxHash, _ := feeBumpTx.HashHex(networkPass) payload := tss.Payload{} payload.WebhookURL = "www.stellar.com" payload.TransactionHash = "hash" @@ -86,8 +90,7 @@ func TestReceive(t *testing.T) { assert.Equal(t, string(tss.NewStatus), status) }) - t.Run("rpc_call_fail", func(t *testing.T) { - txXDR, _ := feeBumpTx.Base64() + t.Run("sign_and_submit_tx_fails", func(t *testing.T) { sendResp := tss.RPCSendTxResponse{} sendResp.Code.OtherCodes = tss.RPCFailCode txServiceMock. @@ -97,7 +100,7 @@ func TestReceive(t *testing.T) { On("NetworkPassphrase"). Return(networkPass). Once(). - On("SendTransaction", txXDR). + On("SendTransaction", feeBumpTxXDR). Return(sendResp, errors.New("RPC Fail")). Once() @@ -116,10 +119,12 @@ func TestReceive(t *testing.T) { }) - t.Run("rpc_resp_unmarshaling_error", func(t *testing.T) { - txXDR, _ := feeBumpTx.Base64() + t.Run("routes_payload", func(t *testing.T) { sendResp := tss.RPCSendTxResponse{} - sendResp.Code.OtherCodes = tss.UnMarshalBinaryCode + sendResp.Status = tss.ErrorStatus + sendResp.TransactionHash = feeBumpTxHash + sendResp.TransactionXDR = feeBumpTxXDR + sendResp.Code.TxResultCode = xdr.TransactionResultCodeTxTooEarly txServiceMock. On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). Return(feeBumpTx, nil). @@ -127,8 +132,12 @@ func TestReceive(t *testing.T) { On("NetworkPassphrase"). Return(networkPass). Once(). - On("SendTransaction", txXDR). - Return(sendResp, errors.New("unable to unmarshal")). + On("SendTransaction", feeBumpTxXDR). + Return(sendResp, nil). + Once() + routerMock. + On("Route", mock.AnythingOfType("tss.Payload")). + Return(). Once() channel.Receive(payload) @@ -136,23 +145,20 @@ func TestReceive(t *testing.T) { var txStatus string err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) require.NoError(t, err) - assert.Equal(t, txStatus, string(tss.NewStatus)) + assert.Equal(t, string(tss.ErrorStatus), txStatus) var tryStatus int - feeBumpTxHash, _ := feeBumpTx.HashHex(networkPass) err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) require.NoError(t, err) - assert.Equal(t, int(tss.UnMarshalBinaryCode), tryStatus) + assert.Equal(t, int(xdr.TransactionResultCodeTxTooEarly), tryStatus) }) - t.Run("rpc_returns_error_response", func(t *testing.T) { - feeBumpTxXDR, _ := feeBumpTx.Base64() - feeBumpTxHash, _ := feeBumpTx.HashHex(networkPass) + t.Run("does_not_routes_payload", func(t *testing.T) { sendResp := tss.RPCSendTxResponse{} - sendResp.Status = tss.ErrorStatus + sendResp.Status = tss.PendingStatus sendResp.TransactionHash = feeBumpTxHash sendResp.TransactionXDR = feeBumpTxXDR - sendResp.Code.TxResultCode = xdr.TransactionResultCodeTxTooEarly + sendResp.Code.TxResultCode = xdr.TransactionResultCodeTxSuccess txServiceMock. On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). Return(feeBumpTx, nil). @@ -163,22 +169,19 @@ func TestReceive(t *testing.T) { On("SendTransaction", feeBumpTxXDR). Return(sendResp, nil). Once() - errHandlerService. - On("ProcessPayload", mock.AnythingOfType("tss.Payload")). - Return(). - Once() + // this time the router mock is not called channel.Receive(payload) var txStatus string err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) require.NoError(t, err) - assert.Equal(t, string(tss.ErrorStatus), txStatus) + assert.Equal(t, string(tss.PendingStatus), txStatus) var tryStatus int err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) require.NoError(t, err) - assert.Equal(t, int(xdr.TransactionResultCodeTxTooEarly), tryStatus) + assert.Equal(t, int(xdr.TransactionResultCodeTxSuccess), tryStatus) }) } diff --git a/internal/tss/channels/utils.go b/internal/tss/channels/utils.go new file mode 100644 index 0000000..a5c59cc --- /dev/null +++ b/internal/tss/channels/utils.go @@ -0,0 +1,46 @@ +package channels + +import ( + "fmt" + + "github.com/stellar/wallet-backend/internal/tss" + "github.com/stellar/wallet-backend/internal/tss/store" + "github.com/stellar/wallet-backend/internal/tss/utils" + "golang.org/x/net/context" +) + +func BuildAndSubmitTransaction(ctx context.Context, channelName string, payload tss.Payload, store store.Store, txService utils.TransactionService) (tss.RPCSendTxResponse, error) { + feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(ctx, payload.TransactionXDR) + if err != nil { + return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to sign/build transaction: %s", channelName, err.Error()) + } + feeBumpTxHash, err := feeBumpTx.HashHex(txService.NetworkPassphrase()) + if err != nil { + return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to hashhex fee bump transaction: %s", channelName, err.Error()) + } + + feeBumpTxXDR, err := feeBumpTx.Base64() + if err != nil { + return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to base64 fee bump transaction: %s", channelName, err.Error()) + } + + err = store.UpsertTry(ctx, payload.TransactionHash, feeBumpTxHash, feeBumpTxXDR, tss.RPCTXCode{OtherCodes: tss.NewCode}) + if err != nil { + return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to upsert try in tries table: %s", channelName, err.Error()) + } + rpcSendResp, rpcErr := txService.SendTransaction(feeBumpTxXDR) + + err = store.UpsertTry(ctx, payload.TransactionHash, feeBumpTxHash, feeBumpTxXDR, rpcSendResp.Code) + if err != nil { + return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to upsert try in tries table: %s", channelName, err.Error()) + } + if rpcErr != nil && rpcSendResp.Code.OtherCodes == tss.RPCFailCode || rpcSendResp.Code.OtherCodes == tss.UnMarshalBinaryCode { + return tss.RPCSendTxResponse{}, fmt.Errorf("%s: RPC fail: %s", channelName, rpcErr.Error()) + } + + err = store.UpsertTransaction(ctx, payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, rpcSendResp.Status) + if err != nil { + return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to do the final update of tx in the transactions table: %s", channelName, err.Error()) + } + return rpcSendResp, nil +} diff --git a/internal/tss/channels/utils_test.go b/internal/tss/channels/utils_test.go new file mode 100644 index 0000000..c3dcb97 --- /dev/null +++ b/internal/tss/channels/utils_test.go @@ -0,0 +1,143 @@ +package channels + +import ( + "context" + "errors" + "testing" + + "github.com/stellar/go/xdr" + "github.com/stellar/wallet-backend/internal/db" + "github.com/stellar/wallet-backend/internal/db/dbtest" + "github.com/stellar/wallet-backend/internal/tss" + "github.com/stellar/wallet-backend/internal/tss/store" + "github.com/stellar/wallet-backend/internal/tss/utils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestBuildAndSubmitTransaction(t *testing.T) { + dbt := dbtest.Open(t) + defer dbt.Close() + + dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) + require.NoError(t, err) + defer dbConnectionPool.Close() + store := store.NewStore(dbConnectionPool) + txServiceMock := utils.TransactionServiceMock{} + networkPass := "passphrase" + feeBumpTx := utils.BuildTestFeeBumpTransaction() + feeBumpTxXDR, _ := feeBumpTx.Base64() + feeBumpTxHash, _ := feeBumpTx.HashHex(networkPass) + payload := tss.Payload{} + payload.WebhookURL = "www.stellar.com" + payload.TransactionHash = "hash" + payload.TransactionXDR = "xdr" + + _ = store.UpsertTransaction(context.Background(), payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.NewStatus) + t.Run("fail_on_tx_build_and_sign", func(t *testing.T) { + txServiceMock. + On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). + Return(nil, errors.New("signing failed")). + Once() + + _, err := BuildAndSubmitTransaction(context.Background(), "channel", payload, store, &txServiceMock) + + assert.Equal(t, "channel: Unable to sign/build transaction: signing failed", err.Error()) + + var status string + err = dbConnectionPool.GetContext(context.Background(), &status, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) + require.NoError(t, err) + assert.Equal(t, string(tss.NewStatus), status) + }) + + t.Run("rpc_call_fail", func(t *testing.T) { + sendResp := tss.RPCSendTxResponse{} + sendResp.Code.OtherCodes = tss.RPCFailCode + txServiceMock. + On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). + Return(feeBumpTx, nil). + Once(). + On("NetworkPassphrase"). + Return(networkPass). + Once(). + On("SendTransaction", feeBumpTxXDR). + Return(sendResp, errors.New("RPC Fail")). + Once() + + _, err := BuildAndSubmitTransaction(context.Background(), "channel", payload, store, &txServiceMock) + + assert.Equal(t, "channel: RPC fail: RPC Fail", err.Error()) + + var txStatus string + err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) + require.NoError(t, err) + assert.Equal(t, txStatus, string(tss.NewStatus)) + + var tryStatus int + err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) + require.NoError(t, err) + assert.Equal(t, int(tss.RPCFailCode), tryStatus) + }) + + t.Run("rpc_resp_unmarshaling_error", func(t *testing.T) { + sendResp := tss.RPCSendTxResponse{} + sendResp.Code.OtherCodes = tss.UnMarshalBinaryCode + txServiceMock. + On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). + Return(feeBumpTx, nil). + Once(). + On("NetworkPassphrase"). + Return(networkPass). + Once(). + On("SendTransaction", feeBumpTxXDR). + Return(sendResp, errors.New("unable to unmarshal")). + Once() + + _, err := BuildAndSubmitTransaction(context.Background(), "channel", payload, store, &txServiceMock) + + assert.Equal(t, "channel: RPC fail: unable to unmarshal", err.Error()) + + var txStatus string + err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) + require.NoError(t, err) + assert.Equal(t, txStatus, string(tss.NewStatus)) + + var tryStatus int + err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) + require.NoError(t, err) + assert.Equal(t, int(tss.UnMarshalBinaryCode), tryStatus) + }) + t.Run("rpc_returns_response", func(t *testing.T) { + sendResp := tss.RPCSendTxResponse{} + sendResp.Status = tss.TryAgainLaterStatus + sendResp.TransactionHash = feeBumpTxHash + sendResp.TransactionXDR = feeBumpTxXDR + sendResp.Code.TxResultCode = xdr.TransactionResultCodeTxInsufficientFee + txServiceMock. + On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). + Return(feeBumpTx, nil). + Once(). + On("NetworkPassphrase"). + Return(networkPass). + Once(). + On("SendTransaction", feeBumpTxXDR). + Return(sendResp, nil). + Once() + + resp, err := BuildAndSubmitTransaction(context.Background(), "channel", payload, store, &txServiceMock) + + assert.Equal(t, tss.TryAgainLaterStatus, resp.Status) + assert.Equal(t, xdr.TransactionResultCodeTxInsufficientFee, resp.Code.TxResultCode) + assert.Empty(t, err) + + var txStatus string + err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) + require.NoError(t, err) + assert.Equal(t, string(tss.TryAgainLaterStatus), txStatus) + + var tryStatus int + err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) + require.NoError(t, err) + assert.Equal(t, int(xdr.TransactionResultCodeTxInsufficientFee), tryStatus) + }) +} diff --git a/internal/tss/router/mocks.go b/internal/tss/router/mocks.go new file mode 100644 index 0000000..3f4406c --- /dev/null +++ b/internal/tss/router/mocks.go @@ -0,0 +1,16 @@ +package router + +import ( + "github.com/stellar/wallet-backend/internal/tss" + "github.com/stretchr/testify/mock" +) + +type MockRouter struct { + mock.Mock +} + +var _ Router = (*MockRouter)(nil) + +func (r *MockRouter) Route(payload tss.Payload) { + r.Called(payload) +} diff --git a/internal/tss/router/router.go b/internal/tss/router/router.go new file mode 100644 index 0000000..e0d3945 --- /dev/null +++ b/internal/tss/router/router.go @@ -0,0 +1,65 @@ +package router + +import ( + "slices" + + "github.com/stellar/go/xdr" + "github.com/stellar/wallet-backend/internal/tss" + "github.com/stellar/wallet-backend/internal/tss/services" +) + +type Router interface { + Route(payload tss.Payload) +} + +type RouterConfigs struct { + ErrorHandlerService services.Service + WebhookHandlerService services.Service +} + +type router struct { + ErrorHandlerService services.Service + WebhookHandlerService services.Service +} + +var _ Router = (*router)(nil) + +var FinalErrorCodes = []xdr.TransactionResultCode{ + xdr.TransactionResultCodeTxSuccess, + xdr.TransactionResultCodeTxFailed, + xdr.TransactionResultCodeTxMissingOperation, + xdr.TransactionResultCodeTxInsufficientBalance, + xdr.TransactionResultCodeTxBadAuthExtra, + xdr.TransactionResultCodeTxMalformed, +} + +var RetryErrorCodes = []xdr.TransactionResultCode{ + xdr.TransactionResultCodeTxTooLate, + xdr.TransactionResultCodeTxInsufficientFee, + xdr.TransactionResultCodeTxInternalError, + xdr.TransactionResultCodeTxBadSeq, +} + +func NewRouter(cfg RouterConfigs) Router { + return &router{ + ErrorHandlerService: cfg.ErrorHandlerService, + WebhookHandlerService: cfg.WebhookHandlerService, + } +} + +func (r *router) Route(payload tss.Payload) { + switch payload.RpcSubmitTxResponse.Status { + case tss.TryAgainLaterStatus: + r.ErrorHandlerService.ProcessPayload(payload) + case tss.ErrorStatus: + if payload.RpcSubmitTxResponse.Code.OtherCodes == tss.NoCode { + if slices.Contains(RetryErrorCodes, payload.RpcSubmitTxResponse.Code.TxResultCode) { + r.ErrorHandlerService.ProcessPayload(payload) + } else if slices.Contains(FinalErrorCodes, payload.RpcSubmitTxResponse.Code.TxResultCode) { + r.WebhookHandlerService.ProcessPayload(payload) + } + } + default: + return + } +} diff --git a/internal/tss/router/router_test.go b/internal/tss/router/router_test.go new file mode 100644 index 0000000..8a5907a --- /dev/null +++ b/internal/tss/router/router_test.go @@ -0,0 +1,51 @@ +package router + +import ( + "testing" + + "github.com/stellar/go/xdr" + "github.com/stellar/wallet-backend/internal/tss" + "github.com/stellar/wallet-backend/internal/tss/services" +) + +func TestRouter(t *testing.T) { + errorHandlerService := services.MockService{} + defer errorHandlerService.AssertExpectations(t) + webhookHandlerService := services.MockService{} + router := NewRouter(RouterConfigs{ErrorHandlerService: &errorHandlerService, WebhookHandlerService: &webhookHandlerService}) + t.Run("status_try_again_later", func(t *testing.T) { + payload := tss.Payload{} + payload.RpcSubmitTxResponse.Status = tss.TryAgainLaterStatus + + errorHandlerService. + On("ProcessPayload", payload). + Return(). + Once() + + router.Route(payload) + }) + t.Run("error_status_route_to_error_handler_service", func(t *testing.T) { + payload := tss.Payload{} + payload.RpcSubmitTxResponse.Status = tss.ErrorStatus + payload.RpcSubmitTxResponse.Code.TxResultCode = xdr.TransactionResultCodeTxInsufficientFee + + errorHandlerService. + On("ProcessPayload", payload). + Return(). + Once() + + router.Route(payload) + }) + t.Run("error_status_route_to_webhook_handler_service", func(t *testing.T) { + payload := tss.Payload{} + payload.RpcSubmitTxResponse.Status = tss.ErrorStatus + payload.RpcSubmitTxResponse.Code.TxResultCode = xdr.TransactionResultCodeTxInsufficientBalance + + webhookHandlerService. + On("ProcessPayload", payload). + Return(). + Once() + + router.Route(payload) + }) +} diff --git a/internal/tss/services/webhook_handler_service.go b/internal/tss/services/webhook_handler_service.go new file mode 100644 index 0000000..33c837e --- /dev/null +++ b/internal/tss/services/webhook_handler_service.go @@ -0,0 +1,19 @@ +package services + +import ( + "github.com/stellar/wallet-backend/internal/tss" +) + +type webhookHandlerService struct { + channel tss.Channel +} + +func NewWebhookHandlerService(channel tss.Channel) Service { + return &webhookHandlerService{ + channel: channel, + } +} + +func (p *webhookHandlerService) ProcessPayload(payload tss.Payload) { + // fill in later +} From a6dc954f6c11866760b36dc522680c24409cf6b2 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Fri, 20 Sep 2024 12:58:48 -0700 Subject: [PATCH 047/113] removing println --- internal/tss/channels/rpc_caller_service_channel_test.go | 2 +- internal/tss/utils/mocks.go | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/internal/tss/channels/rpc_caller_service_channel_test.go b/internal/tss/channels/rpc_caller_service_channel_test.go index d9abc5e..276080a 100644 --- a/internal/tss/channels/rpc_caller_service_channel_test.go +++ b/internal/tss/channels/rpc_caller_service_channel_test.go @@ -37,7 +37,7 @@ func TestSend(t *testing.T) { payload.TransactionHash = "hash" payload.TransactionXDR = "xdr" txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", payload.TransactionXDR). + On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). Return(nil, errors.New("signing failed")) channel.Send(payload) channel.Stop() diff --git a/internal/tss/utils/mocks.go b/internal/tss/utils/mocks.go index f090df6..597a6c9 100644 --- a/internal/tss/utils/mocks.go +++ b/internal/tss/utils/mocks.go @@ -2,7 +2,6 @@ package utils import ( "context" - "fmt" "io" "net/http" @@ -32,7 +31,6 @@ func (t *TransactionServiceMock) NetworkPassphrase() string { } func (t *TransactionServiceMock) SignAndBuildNewFeeBumpTransaction(ctx context.Context, origTxXdr string) (*txnbuild.FeeBumpTransaction, error) { - fmt.Println("INSIDE SignAndBuildNewFeeBumpTransaction mock") args := t.Called(ctx, origTxXdr) if result := args.Get(0); result != nil { return result.(*txnbuild.FeeBumpTransaction), args.Error(1) From e28ff02e064e9001cce50f025dbc38bc2066ac82 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Fri, 20 Sep 2024 13:05:54 -0700 Subject: [PATCH 048/113] changed function name --- .../channels/error_handler_service_jitter_channel.go | 2 +- .../error_handler_service_non_jitter_channel.go | 2 +- .../error_handler_service_non_jitter_channel_test.go | 2 +- .../error_service_handler_jitter_channel_test.go | 2 +- internal/tss/channels/utils.go | 2 +- internal/tss/channels/utils_test.go | 10 +++++----- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/internal/tss/channels/error_handler_service_jitter_channel.go b/internal/tss/channels/error_handler_service_jitter_channel.go index 346493b..09cd8a0 100644 --- a/internal/tss/channels/error_handler_service_jitter_channel.go +++ b/internal/tss/channels/error_handler_service_jitter_channel.go @@ -62,7 +62,7 @@ func (p *rpcErrorHandlerServiceJitterPool) Receive(payload tss.Payload) { for i = 0; i < p.MaxRetries; i++ { currentBackoff := p.MinWaitBtwnRetriesMS * (1 << i) sleep(jitter(time.Duration(currentBackoff)) * time.Microsecond) - rpcSendResp, err := SignAndSubmitTransaction(ctx, "ErrorHandlerServiceJitterChannel", payload, p.Store, p.TxService) + rpcSendResp, err := BuildAndSubmitTransaction(ctx, "ErrorHandlerServiceJitterChannel", payload, p.Store, p.TxService) if err != nil { log.Errorf(err.Error()) return diff --git a/internal/tss/channels/error_handler_service_non_jitter_channel.go b/internal/tss/channels/error_handler_service_non_jitter_channel.go index 1bb9469..d572f00 100644 --- a/internal/tss/channels/error_handler_service_non_jitter_channel.go +++ b/internal/tss/channels/error_handler_service_non_jitter_channel.go @@ -54,7 +54,7 @@ func (p *rpcErrorHandlerServiceNonJitterPool) Receive(payload tss.Payload) { var i int for i = 0; i < p.MaxRetries; i++ { sleep(time.Duration(p.WaitBtwnRetriesMS) * time.Microsecond) - rpcSendResp, err := SignAndSubmitTransaction(ctx, "ErrorHandlerServiceNonJitterChannel", payload, p.Store, p.TxService) + rpcSendResp, err := BuildAndSubmitTransaction(ctx, "ErrorHandlerServiceNonJitterChannel", payload, p.Store, p.TxService) if err != nil { log.Errorf(err.Error()) return diff --git a/internal/tss/channels/error_handler_service_non_jitter_channel_test.go b/internal/tss/channels/error_handler_service_non_jitter_channel_test.go index daa0461..083a7a6 100644 --- a/internal/tss/channels/error_handler_service_non_jitter_channel_test.go +++ b/internal/tss/channels/error_handler_service_non_jitter_channel_test.go @@ -42,7 +42,7 @@ func TestNonJitterSend(t *testing.T) { payload.TransactionHash = "hash" payload.TransactionXDR = "xdr" txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", payload.TransactionXDR). + On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). Return(nil, errors.New("signing failed")) _ = store.UpsertTransaction(context.Background(), payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.NewStatus) diff --git a/internal/tss/channels/error_service_handler_jitter_channel_test.go b/internal/tss/channels/error_service_handler_jitter_channel_test.go index 13ef183..f0023c3 100644 --- a/internal/tss/channels/error_service_handler_jitter_channel_test.go +++ b/internal/tss/channels/error_service_handler_jitter_channel_test.go @@ -42,7 +42,7 @@ func TestJitterSend(t *testing.T) { payload.TransactionHash = "hash" payload.TransactionXDR = "xdr" txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", payload.TransactionXDR). + On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). Return(nil, errors.New("signing failed")) _ = store.UpsertTransaction(context.Background(), payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.NewStatus) diff --git a/internal/tss/channels/utils.go b/internal/tss/channels/utils.go index 3db70da..a5c59cc 100644 --- a/internal/tss/channels/utils.go +++ b/internal/tss/channels/utils.go @@ -9,7 +9,7 @@ import ( "golang.org/x/net/context" ) -func SignAndSubmitTransaction(ctx context.Context, channelName string, payload tss.Payload, store store.Store, txService utils.TransactionService) (tss.RPCSendTxResponse, error) { +func BuildAndSubmitTransaction(ctx context.Context, channelName string, payload tss.Payload, store store.Store, txService utils.TransactionService) (tss.RPCSendTxResponse, error) { feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(ctx, payload.TransactionXDR) if err != nil { return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to sign/build transaction: %s", channelName, err.Error()) diff --git a/internal/tss/channels/utils_test.go b/internal/tss/channels/utils_test.go index 62c2dba..c3dcb97 100644 --- a/internal/tss/channels/utils_test.go +++ b/internal/tss/channels/utils_test.go @@ -15,7 +15,7 @@ import ( "github.com/stretchr/testify/require" ) -func TestSignAndSubmitTransaction(t *testing.T) { +func TestBuildAndSubmitTransaction(t *testing.T) { dbt := dbtest.Open(t) defer dbt.Close() @@ -40,7 +40,7 @@ func TestSignAndSubmitTransaction(t *testing.T) { Return(nil, errors.New("signing failed")). Once() - _, err := SignAndSubmitTransaction(context.Background(), "channel", payload, store, &txServiceMock) + _, err := BuildAndSubmitTransaction(context.Background(), "channel", payload, store, &txServiceMock) assert.Equal(t, "channel: Unable to sign/build transaction: signing failed", err.Error()) @@ -64,7 +64,7 @@ func TestSignAndSubmitTransaction(t *testing.T) { Return(sendResp, errors.New("RPC Fail")). Once() - _, err := SignAndSubmitTransaction(context.Background(), "channel", payload, store, &txServiceMock) + _, err := BuildAndSubmitTransaction(context.Background(), "channel", payload, store, &txServiceMock) assert.Equal(t, "channel: RPC fail: RPC Fail", err.Error()) @@ -93,7 +93,7 @@ func TestSignAndSubmitTransaction(t *testing.T) { Return(sendResp, errors.New("unable to unmarshal")). Once() - _, err := SignAndSubmitTransaction(context.Background(), "channel", payload, store, &txServiceMock) + _, err := BuildAndSubmitTransaction(context.Background(), "channel", payload, store, &txServiceMock) assert.Equal(t, "channel: RPC fail: unable to unmarshal", err.Error()) @@ -124,7 +124,7 @@ func TestSignAndSubmitTransaction(t *testing.T) { Return(sendResp, nil). Once() - resp, err := SignAndSubmitTransaction(context.Background(), "channel", payload, store, &txServiceMock) + resp, err := BuildAndSubmitTransaction(context.Background(), "channel", payload, store, &txServiceMock) assert.Equal(t, tss.TryAgainLaterStatus, resp.Status) assert.Equal(t, xdr.TransactionResultCodeTxInsufficientFee, resp.Code.TxResultCode) From 3d6960198751d3c8b21c33804d0c819dd1db3ad9 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Sat, 21 Sep 2024 14:52:03 -0700 Subject: [PATCH 049/113] commit #1 --- .env.swp | Bin 0 -> 12288 bytes .../error_handler_service_jitter_channel.go | 7 -- internal/tss/channels/utils.go | 8 ++ .../webhook_handler_service_channel.go | 74 ++++++++++++++ .../webhook_handler_service_channel_test.go | 54 ++++++++++ internal/tss/router/router.go | 2 + internal/tss/types.go | 12 +++ internal/tss/utils/helpers.go | 17 ++++ internal/tss/utils/transaction_service.go | 7 +- .../tss/utils/transaction_service_test.go | 94 ++++++++++++------ 10 files changed, 232 insertions(+), 43 deletions(-) create mode 100644 .env.swp create mode 100644 internal/tss/channels/webhook_handler_service_channel.go create mode 100644 internal/tss/channels/webhook_handler_service_channel_test.go diff --git a/.env.swp b/.env.swp new file mode 100644 index 0000000000000000000000000000000000000000..a9c4a2932ccedb929712698350c7f7a9cf316c3a GIT binary patch literal 12288 zcmeI&O^ee&7y#f|FCN72yNvF6(>9;2P%ur>G;K3U^VJW~lD5-r*lbdh)Y^-l#ETyN zFa9M#|AX1my;uZc@v!g?ybPJlJDGVx4nt;E4Ga@Em?|pEBZOwZek?yddyhWfMd&=; z6sz!(S%*auMi-kTy6SXXj{BW;6uZGzIk7f6yPT~LDIbnIR`PPn-6%ann`Xp41My9C3xNKX7Ef+nR{3$C_5V zr?o^y<=sKomQ1NLW(BL;?pY(lopk;(vft_(%6?#7wTY<{Zz$?IX}LT%R0DV9jV4+! z;8;=QM(R*9bVKKD&9$TnYZAkk2#Zd!P7|7i1;rsw=oVkl-8zgiyi7A(tSDYYd67l4 zO%bID4(Ic9lN1a>L%HAAys2yG#31^AnnQUUzrfolj&UBHC;REF!(E()3%p&?1ZVW^ jrhH4~yDINRIX)bxXRy3j;1$j2KE>Y5;%JV0bjP6I$zk+B literal 0 HcmV?d00001 diff --git a/internal/tss/channels/error_handler_service_jitter_channel.go b/internal/tss/channels/error_handler_service_jitter_channel.go index 09cd8a0..00fdda8 100644 --- a/internal/tss/channels/error_handler_service_jitter_channel.go +++ b/internal/tss/channels/error_handler_service_jitter_channel.go @@ -11,7 +11,6 @@ import ( "github.com/stellar/wallet-backend/internal/tss/router" tss_store "github.com/stellar/wallet-backend/internal/tss/store" "github.com/stellar/wallet-backend/internal/tss/utils" - "golang.org/x/exp/rand" ) type RPCErrorHandlerServiceJitterChannelConfigs struct { @@ -33,12 +32,6 @@ type rpcErrorHandlerServiceJitterPool struct { MinWaitBtwnRetriesMS int } -func jitter(dur time.Duration) time.Duration { - halfDur := int64(dur / 2) - delta := rand.Int63n(halfDur) - halfDur/2 - return dur + time.Duration(delta) -} - func NewErrorHandlerServiceJitterChannel(cfg RPCErrorHandlerServiceJitterChannelConfigs) *rpcErrorHandlerServiceJitterPool { pool := pond.New(cfg.MaxBufferSize, cfg.MaxWorkers, pond.Strategy(pond.Balanced())) return &rpcErrorHandlerServiceJitterPool{ diff --git a/internal/tss/channels/utils.go b/internal/tss/channels/utils.go index a5c59cc..3da7d9a 100644 --- a/internal/tss/channels/utils.go +++ b/internal/tss/channels/utils.go @@ -2,13 +2,21 @@ package channels import ( "fmt" + "time" "github.com/stellar/wallet-backend/internal/tss" "github.com/stellar/wallet-backend/internal/tss/store" "github.com/stellar/wallet-backend/internal/tss/utils" + "golang.org/x/exp/rand" "golang.org/x/net/context" ) +func jitter(dur time.Duration) time.Duration { + halfDur := int64(dur / 2) + delta := rand.Int63n(halfDur) - halfDur/2 + return dur + time.Duration(delta) +} + func BuildAndSubmitTransaction(ctx context.Context, channelName string, payload tss.Payload, store store.Store, txService utils.TransactionService) (tss.RPCSendTxResponse, error) { feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(ctx, payload.TransactionXDR) if err != nil { diff --git a/internal/tss/channels/webhook_handler_service_channel.go b/internal/tss/channels/webhook_handler_service_channel.go new file mode 100644 index 0000000..b833992 --- /dev/null +++ b/internal/tss/channels/webhook_handler_service_channel.go @@ -0,0 +1,74 @@ +package channels + +import ( + "bytes" + "encoding/json" + "net/http" + "time" + + "github.com/alitto/pond" + "github.com/stellar/go/support/log" + "github.com/stellar/wallet-backend/internal/tss" + "github.com/stellar/wallet-backend/internal/tss/utils" +) + +type WebhookHandlerServiceChannelConfigs struct { + HTTPClient utils.HTTPClient + MaxBufferSize int + MaxWorkers int + MaxRetries int + MinWaitBtwnRetriesMS int +} + +type webhookHandlerServicePool struct { + Pool *pond.WorkerPool + HTTPClient utils.HTTPClient + MaxRetries int + MinWaitBtwnRetriesMS int +} + +var _ tss.Channel = (*webhookHandlerServicePool)(nil) + +func NewWebhookHandlerServiceChannel(cfg WebhookHandlerServiceChannelConfigs) *webhookHandlerServicePool { + pool := pond.New(cfg.MaxBufferSize, cfg.MaxWorkers, pond.Strategy(pond.Balanced())) + return &webhookHandlerServicePool{ + Pool: pool, + HTTPClient: cfg.HTTPClient, + MaxRetries: cfg.MaxRetries, + MinWaitBtwnRetriesMS: cfg.MinWaitBtwnRetriesMS, + } + +} + +func (p *webhookHandlerServicePool) Send(payload tss.Payload) { + p.Pool.Submit(func() { + p.Receive(payload) + }) +} + +func (p *webhookHandlerServicePool) Receive(payload tss.Payload) { + resp := utils.PayloadTOTSSResponse(payload) + jsonData, err := json.Marshal(resp) + if err != nil { + log.Errorf("WebhookHandlerServiceChannel: error marshaling payload: %s", err.Error()) + return + } + var i int + for i = 0; i < p.MaxRetries; i++ { + resp, err := p.HTTPClient.Post(payload.WebhookURL, "application/json", bytes.NewBuffer(jsonData)) + if err != nil { + log.Errorf("WebhookHandlerServiceChannel: error making POST request to webhook: %s", err.Error()) + } + defer resp.Body.Close() + + if resp.StatusCode == http.StatusOK { + return + } + currentBackoff := p.MinWaitBtwnRetriesMS * (1 << i) + sleep(jitter(time.Duration(currentBackoff)) * time.Microsecond) + } +} + +func (p *webhookHandlerServicePool) Stop() { + p.Pool.StopAndWait() +} diff --git a/internal/tss/channels/webhook_handler_service_channel_test.go b/internal/tss/channels/webhook_handler_service_channel_test.go new file mode 100644 index 0000000..df391e4 --- /dev/null +++ b/internal/tss/channels/webhook_handler_service_channel_test.go @@ -0,0 +1,54 @@ +package channels + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + "strings" + "testing" + + "github.com/stellar/wallet-backend/internal/tss" + "github.com/stellar/wallet-backend/internal/tss/utils" +) + +func TestWebhookHandlerServiceChannel(t *testing.T) { + mockHTTPClient := utils.MockHTTPClient{} + cfg := WebhookHandlerServiceChannelConfigs{ + HTTPClient: &mockHTTPClient, + MaxBufferSize: 1, + MaxWorkers: 1, + MaxRetries: 3, + MinWaitBtwnRetriesMS: 5, + } + channel := NewWebhookHandlerServiceChannel(cfg) + + payload := tss.Payload{} + payload.WebhookURL = "www.stellar.org" + jsonData, _ := json.Marshal(utils.PayloadTOTSSResponse(payload)) + + httpResponse1 := &http.Response{ + StatusCode: http.StatusBadGateway, + Body: io.NopCloser(strings.NewReader(`{"result": {"status": "OK"}}`)), + } + + httpResponse2 := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"result": {"status": "OK"}}`)), + } + + mockHTTPClient. + On("Post", payload.WebhookURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse1, nil). + Once() + + mockHTTPClient. + On("Post", payload.WebhookURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse2, nil). + Once() + + channel.Send(payload) + channel.Stop() + + mockHTTPClient.AssertNumberOfCalls(t, "Post", 2) +} diff --git a/internal/tss/router/router.go b/internal/tss/router/router.go index e0d3945..dcbd2db 100644 --- a/internal/tss/router/router.go +++ b/internal/tss/router/router.go @@ -59,7 +59,9 @@ func (r *router) Route(payload tss.Payload) { r.WebhookHandlerService.ProcessPayload(payload) } } + // if Code.OtherCodes = {RPCFailCode, UnMarshall, do nothing, as this should be rare. Let the ticker task take care of this} default: + // PENDING = wait to ingest this transaction via getTransactions() return } } diff --git a/internal/tss/types.go b/internal/tss/types.go index 0489bf6..c4bf00d 100644 --- a/internal/tss/types.go +++ b/internal/tss/types.go @@ -49,6 +49,9 @@ var JitterErrorCodes = []xdr.TransactionResultCode{ type RPCGetIngestTxResponse struct { // A status that indicated whether this transaction failed or successly made it to the ledger Status RPCTXStatus + // The error code that is derived by deserialzing the ResultXdr string in the sendTransaction response + // list of possible errror codes: https://developers.stellar.org/docs/data/horizon/api-reference/errors/result-codes/transactions + Code RPCTXCode // The raw TransactionEnvelope XDR for this transaction EnvelopeXDR string // The raw TransactionResult XDR of the envelopeXdr @@ -93,6 +96,15 @@ type RPCResponse struct { RPCResult `json:"result"` } +type TSSResponse struct { + TransactionHash string `json:"tx_hash"` + TransactionResultCode string `json:"tx_result_code"` + Status string `json:"status"` + CreatedAt int64 `json:"created_at"` + EnvelopeXDR string `json:"envelopeXdr"` + ResultXDR string `json:"resultXdr"` +} + type Channel interface { Send(payload Payload) Receive(payload Payload) diff --git a/internal/tss/utils/helpers.go b/internal/tss/utils/helpers.go index c844a03..81cb448 100644 --- a/internal/tss/utils/helpers.go +++ b/internal/tss/utils/helpers.go @@ -3,8 +3,25 @@ package utils import ( "github.com/stellar/go/keypair" "github.com/stellar/go/txnbuild" + "github.com/stellar/wallet-backend/internal/tss" ) +func PayloadTOTSSResponse(payload tss.Payload) tss.TSSResponse { + response := tss.TSSResponse{} + response.TransactionHash = payload.TransactionHash + if payload.RpcSubmitTxResponse.Status != "" { + response.Status = string(payload.RpcSubmitTxResponse.Status) + response.TransactionResultCode = payload.RpcSubmitTxResponse.Code.TxResultCode.String() + response.EnvelopeXDR = payload.RpcSubmitTxResponse.TransactionXDR + } else if payload.RpcGetIngestTxResponse.Status != "" { + response.Status = string(payload.RpcGetIngestTxResponse.Status) + response.TransactionResultCode = payload.RpcGetIngestTxResponse.Code.TxResultCode.String() + response.EnvelopeXDR = payload.RpcGetIngestTxResponse.EnvelopeXDR + response.ResultXDR = payload.RpcGetIngestTxResponse.ResultXDR + } + return response +} + func BuildTestTransaction() *txnbuild.Transaction { accountToSponsor := keypair.MustRandom() diff --git a/internal/tss/utils/transaction_service.go b/internal/tss/utils/transaction_service.go index 83e5643..129c1f0 100644 --- a/internal/tss/utils/transaction_service.go +++ b/internal/tss/utils/transaction_service.go @@ -199,9 +199,6 @@ func (t *transactionService) sendRPCRequest(method string, params map[string]str if err != nil { return tss.RPCResponse{}, fmt.Errorf("%s: parsing RPC response JSON", method) } - if res.RPCResult == (tss.RPCResult{}) { - return tss.RPCResponse{}, fmt.Errorf("%s: response missing result field", method) - } return res, nil } @@ -210,6 +207,7 @@ func (t *transactionService) SendTransaction(transactionXdr string) (tss.RPCSend sendTxResponse := tss.RPCSendTxResponse{} sendTxResponse.TransactionXDR = transactionXdr if err != nil { + sendTxResponse.Status = tss.ErrorStatus sendTxResponse.Code.OtherCodes = tss.RPCFailCode return sendTxResponse, fmt.Errorf("RPC fail: %s", err.Error()) } @@ -234,5 +232,6 @@ func (t *transactionService) GetTransaction(transactionHash string) (tss.RPCGetI return tss.RPCGetIngestTxResponse{Status: tss.ErrorStatus}, fmt.Errorf("unable to parse createAt: %s", err.Error()) } } - return getIngestTxResponse, nil + getIngestTxResponse.Code, err = t.parseErrorResultXDR(rpcResponse.RPCResult.ResultXDR) + return getIngestTxResponse, err } diff --git a/internal/tss/utils/transaction_service_test.go b/internal/tss/utils/transaction_service_test.go index 8a94f86..3ef6372 100644 --- a/internal/tss/utils/transaction_service_test.go +++ b/internal/tss/utils/transaction_service_test.go @@ -23,28 +23,6 @@ import ( "github.com/stretchr/testify/mock" ) -func buildTestTransaction() *txnbuild.Transaction { - accountToSponsor := keypair.MustRandom() - - tx, _ := txnbuild.NewTransaction(txnbuild.TransactionParams{ - SourceAccount: &txnbuild.SimpleAccount{ - AccountID: accountToSponsor.Address(), - Sequence: 124, - }, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - &txnbuild.Payment{ - Destination: keypair.MustRandom().Address(), - Amount: "14", - Asset: txnbuild.NativeAsset{}, - }, - }, - BaseFee: 104, - Preconditions: txnbuild.Preconditions{TimeBounds: txnbuild.NewTimeout(10)}, - }) - return tx -} - func TestValidateOptions(t *testing.T) { t.Run("return_error_when_distribution_signature_client_nil", func(t *testing.T) { opts := TransactionServiceOptions{ @@ -141,7 +119,7 @@ func TestSignAndBuildNewFeeBumpTransaction(t *testing.T) { HTTPClient: &MockHTTPClient{}, }) - txStr, _ := buildTestTransaction().Base64() + txStr, _ := BuildTestTransaction().Base64() t.Run("malformed_transaction_string", func(t *testing.T) { feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), "abcd") @@ -231,7 +209,7 @@ func TestSignAndBuildNewFeeBumpTransaction(t *testing.T) { t.Run("horizon_client_sign_stellar_transaction_w_distribition_account_err", func(t *testing.T) { account := keypair.MustRandom() - signedTx := buildTestTransaction() + signedTx := BuildTestTransaction() channelAccountSignatureClient. On("GetAccountPublicKey", context.Background()). Return(account.Address(), nil). @@ -262,7 +240,7 @@ func TestSignAndBuildNewFeeBumpTransaction(t *testing.T) { t.Run("returns_signed_tx", func(t *testing.T) { account := keypair.MustRandom() - signedTx := buildTestTransaction() + signedTx := BuildTestTransaction() testFeeBumpTx, _ := txnbuild.NewFeeBumpTransaction( txnbuild.FeeBumpTransactionParams{ Inner: signedTx, @@ -299,6 +277,39 @@ func TestSignAndBuildNewFeeBumpTransaction(t *testing.T) { }) } +func TestParseErrorResultXDR(t *testing.T) { + distributionAccountSignatureClient := signing.SignatureClientMock{} + defer distributionAccountSignatureClient.AssertExpectations(t) + channelAccountSignatureClient := signing.SignatureClientMock{} + defer channelAccountSignatureClient.AssertExpectations(t) + horizonClient := horizonclient.MockClient{} + defer horizonClient.AssertExpectations(t) + txService, _ := NewTransactionService(TransactionServiceOptions{ + DistributionAccountSignatureClient: &distributionAccountSignatureClient, + ChannelAccountSignatureClient: &channelAccountSignatureClient, + HorizonClient: &horizonClient, + RPCURL: "http://localhost:8000/soroban/rpc", + BaseFee: 114, + HTTPClient: &MockHTTPClient{}, + }) + + t.Run("errorResultXdr_empty", func(t *testing.T) { + _, err := txService.parseErrorResultXDR("") + assert.Equal(t, "unable to unmarshal errorResultXdr: ", err.Error()) + }) + + t.Run("errorResultXdr_invalid", func(t *testing.T) { + _, err := txService.parseErrorResultXDR("ABCD") + assert.Equal(t, "unable to unmarshal errorResultXdr: ABCD", err.Error()) + }) + + t.Run("errorResultXdr_valid", func(t *testing.T) { + resp, err := txService.parseErrorResultXDR("AAAAAAAAAMj////9AAAAAA==") + assert.Equal(t, xdr.TransactionResultCodeTxTooLate, resp.TxResultCode) + assert.Empty(t, err) + }) +} + type errorReader struct{} func (e *errorReader) Read(p []byte) (n int, err error) { @@ -383,10 +394,8 @@ func TestSendRPCRequest(t *testing.T) { Return(httpResponse, nil). Once() - resp, err := txService.sendRPCRequest(method, params) - + resp, _ := txService.sendRPCRequest(method, params) assert.Empty(t, resp) - assert.Equal(t, "sendTransaction: response missing result field", err.Error()) }) t.Run("response_has_status_field", func(t *testing.T) { @@ -515,6 +524,7 @@ func TestSendTransaction(t *testing.T) { resp, err := txService.SendTransaction("ABCD") + assert.Equal(t, tss.ErrorStatus, resp.Status) assert.Equal(t, tss.RPCFailCode, resp.Code.OtherCodes) assert.Equal(t, "RPC fail: sendTransaction: sending POST request to rpc: RPC Connection fail", err.Error()) @@ -522,7 +532,7 @@ func TestSendTransaction(t *testing.T) { t.Run("response_has_unparsable_errorResultXdr", func(t *testing.T) { httpResponse := &http.Response{ StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"errorResultXdr": "ABC123"}}`)), + Body: io.NopCloser(strings.NewReader(`{"result": {"status": "ERROR", "errorResultXdr": "ABC123"}}`)), } mockHTTPClient. On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). @@ -531,13 +541,30 @@ func TestSendTransaction(t *testing.T) { resp, err := txService.SendTransaction("ABCD") + assert.Equal(t, tss.ErrorStatus, resp.Status) assert.Equal(t, tss.UnMarshalBinaryCode, resp.Code.OtherCodes) assert.Equal(t, "unable to unmarshal errorResultXdr: ABC123", err.Error()) }) + t.Run("response_has_empty_errorResultXdr_wth_status", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"result": {"status": "PENDING", "errorResultXdr": ""}}`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). + Once() + + resp, err := txService.SendTransaction("ABCD") + + assert.Equal(t, tss.PendingStatus, resp.Status) + assert.Equal(t, tss.UnMarshalBinaryCode, resp.Code.OtherCodes) + assert.Equal(t, "unable to unmarshal errorResultXdr: ", err.Error()) + }) t.Run("response_has_errorResultXdr", func(t *testing.T) { httpResponse := &http.Response{ StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"errorResultXdr": "AAAAAAAAAMj////9AAAAAA=="}}`)), + Body: io.NopCloser(strings.NewReader(`{"result": {"status": "ERROR", "errorResultXdr": "AAAAAAAAAMj////9AAAAAA=="}}`)), } mockHTTPClient. On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). @@ -546,6 +573,7 @@ func TestSendTransaction(t *testing.T) { resp, err := txService.SendTransaction("ABCD") + assert.Equal(t, tss.ErrorStatus, resp.Status) assert.Equal(t, xdr.TransactionResultCodeTxTooLate, resp.Code.TxResultCode) assert.Empty(t, err) }) @@ -599,10 +627,10 @@ func TestGetTransaction(t *testing.T) { assert.Equal(t, tss.ErrorStatus, resp.Status) assert.Equal(t, "unable to parse createAt: strconv.ParseInt: parsing \"ABCD\": invalid syntax", err.Error()) }) - t.Run("response_has_createdAt_field", func(t *testing.T) { + t.Run("response_has_createdAt_resultXdr_field", func(t *testing.T) { httpResponse := &http.Response{ StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"createdAt": "1234567"}}`)), + Body: io.NopCloser(strings.NewReader(`{"result": {"status": "FAILED", "resultXdr": "AAAAAAAAAMj////9AAAAAA==", "createdAt": "1234567"}}`)), } mockHTTPClient. On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). @@ -611,6 +639,8 @@ func TestGetTransaction(t *testing.T) { resp, err := txService.GetTransaction("XYZ") + assert.Equal(t, tss.FailedStatus, resp.Status) + assert.Equal(t, xdr.TransactionResultCodeTxTooLate, resp.Code.TxResultCode) assert.Equal(t, int64(1234567), resp.CreatedAt) assert.Empty(t, err) }) From 43fcf10b0ada2ff0a506afec75dce413568b673e Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Sat, 21 Sep 2024 15:00:07 -0700 Subject: [PATCH 050/113] Code() helper function on RPCTXCode --- internal/tss/store/store.go | 9 +-------- internal/tss/types.go | 7 +++++++ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/internal/tss/store/store.go b/internal/tss/store/store.go index 5dcc97f..866f1cc 100644 --- a/internal/tss/store/store.go +++ b/internal/tss/store/store.go @@ -58,14 +58,7 @@ func (s *store) UpsertTry(ctx context.Context, txHash string, feeBumpTxHash stri status = $4, updated_at = NOW(); ` - var st int - // if this value is set, it takes precedence over the code from RPC - if status.OtherCodes != tss.NoCode { - st = int(status.OtherCodes) - } else { - st = int(status.TxResultCode) - } - _, err := s.DB.ExecContext(ctx, q, txHash, feeBumpTxHash, feeBumpTxXDR, st) + _, err := s.DB.ExecContext(ctx, q, txHash, feeBumpTxHash, feeBumpTxXDR, status.Code()) if err != nil { return fmt.Errorf("inserting/updating tss try: %w", err) } diff --git a/internal/tss/types.go b/internal/tss/types.go index 9d5f6a4..d3654ab 100644 --- a/internal/tss/types.go +++ b/internal/tss/types.go @@ -21,6 +21,13 @@ type RPCTXCode struct { OtherCodes OtherCodes } +func (c RPCTXCode) Code() int { + if c.OtherCodes != NoCode { + return int(c.OtherCodes) + } + return int(c.TxResultCode) +} + const ( // Brand new transaction, not sent to RPC yet NewStatus RPCTXStatus = "NEW" From 4793f90c2bcfa834f7527f1ff3aaf9f4e3912adb Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Sat, 21 Sep 2024 15:15:08 -0700 Subject: [PATCH 051/113] Code() --- internal/tss/store/store.go | 9 +-------- internal/tss/types.go | 7 +++++++ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/internal/tss/store/store.go b/internal/tss/store/store.go index 5dcc97f..866f1cc 100644 --- a/internal/tss/store/store.go +++ b/internal/tss/store/store.go @@ -58,14 +58,7 @@ func (s *store) UpsertTry(ctx context.Context, txHash string, feeBumpTxHash stri status = $4, updated_at = NOW(); ` - var st int - // if this value is set, it takes precedence over the code from RPC - if status.OtherCodes != tss.NoCode { - st = int(status.OtherCodes) - } else { - st = int(status.TxResultCode) - } - _, err := s.DB.ExecContext(ctx, q, txHash, feeBumpTxHash, feeBumpTxXDR, st) + _, err := s.DB.ExecContext(ctx, q, txHash, feeBumpTxHash, feeBumpTxXDR, status.Code()) if err != nil { return fmt.Errorf("inserting/updating tss try: %w", err) } diff --git a/internal/tss/types.go b/internal/tss/types.go index 0489bf6..372dc6e 100644 --- a/internal/tss/types.go +++ b/internal/tss/types.go @@ -21,6 +21,13 @@ type RPCTXCode struct { OtherCodes OtherCodes } +func (c RPCTXCode) Code() int { + if c.OtherCodes != NoCode { + return int(c.OtherCodes) + } + return int(c.TxResultCode) +} + const ( // Brand new transaction, not sent to RPC yet NewStatus RPCTXStatus = "NEW" From b5d46dadcebd92d89fa0c1c55d358d05169da8c7 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Sat, 21 Sep 2024 15:16:58 -0700 Subject: [PATCH 052/113] Delete .env.swp --- .env.swp | Bin 12288 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .env.swp diff --git a/.env.swp b/.env.swp deleted file mode 100644 index a9c4a2932ccedb929712698350c7f7a9cf316c3a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12288 zcmeI&O^ee&7y#f|FCN72yNvF6(>9;2P%ur>G;K3U^VJW~lD5-r*lbdh)Y^-l#ETyN zFa9M#|AX1my;uZc@v!g?ybPJlJDGVx4nt;E4Ga@Em?|pEBZOwZek?yddyhWfMd&=; z6sz!(S%*auMi-kTy6SXXj{BW;6uZGzIk7f6yPT~LDIbnIR`PPn-6%ann`Xp41My9C3xNKX7Ef+nR{3$C_5V zr?o^y<=sKomQ1NLW(BL;?pY(lopk;(vft_(%6?#7wTY<{Zz$?IX}LT%R0DV9jV4+! z;8;=QM(R*9bVKKD&9$TnYZAkk2#Zd!P7|7i1;rsw=oVkl-8zgiyi7A(tSDYYd67l4 zO%bID4(Ic9lN1a>L%HAAys2yG#31^AnnQUUzrfolj&UBHC;REF!(E()3%p&?1ZVW^ jrhH4~yDINRIX)bxXRy3j;1$j2KE>Y5;%JV0bjP6I$zk+B From b4ab5de408ed16adfe94fb414c30ead0f546237d Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Sat, 21 Sep 2024 16:06:51 -0700 Subject: [PATCH 053/113] adding a helpers file --- internal/tss/utils/helpers.go | 39 +++++++++++++++++++ .../tss/utils/transaction_service_test.go | 28 ++----------- 2 files changed, 42 insertions(+), 25 deletions(-) create mode 100644 internal/tss/utils/helpers.go diff --git a/internal/tss/utils/helpers.go b/internal/tss/utils/helpers.go new file mode 100644 index 0000000..c844a03 --- /dev/null +++ b/internal/tss/utils/helpers.go @@ -0,0 +1,39 @@ +package utils + +import ( + "github.com/stellar/go/keypair" + "github.com/stellar/go/txnbuild" +) + +func BuildTestTransaction() *txnbuild.Transaction { + accountToSponsor := keypair.MustRandom() + + tx, _ := txnbuild.NewTransaction(txnbuild.TransactionParams{ + SourceAccount: &txnbuild.SimpleAccount{ + AccountID: accountToSponsor.Address(), + Sequence: 124, + }, + IncrementSequenceNum: true, + Operations: []txnbuild.Operation{ + &txnbuild.Payment{ + Destination: keypair.MustRandom().Address(), + Amount: "14", + Asset: txnbuild.NativeAsset{}, + }, + }, + BaseFee: 104, + Preconditions: txnbuild.Preconditions{TimeBounds: txnbuild.NewTimeout(10)}, + }) + return tx +} + +func BuildTestFeeBumpTransaction() *txnbuild.FeeBumpTransaction { + + feeBumpTx, _ := txnbuild.NewFeeBumpTransaction( + txnbuild.FeeBumpTransactionParams{ + Inner: BuildTestTransaction(), + FeeAccount: keypair.MustRandom().Address(), + BaseFee: 110, + }) + return feeBumpTx +} diff --git a/internal/tss/utils/transaction_service_test.go b/internal/tss/utils/transaction_service_test.go index 8a94f86..f924f01 100644 --- a/internal/tss/utils/transaction_service_test.go +++ b/internal/tss/utils/transaction_service_test.go @@ -23,28 +23,6 @@ import ( "github.com/stretchr/testify/mock" ) -func buildTestTransaction() *txnbuild.Transaction { - accountToSponsor := keypair.MustRandom() - - tx, _ := txnbuild.NewTransaction(txnbuild.TransactionParams{ - SourceAccount: &txnbuild.SimpleAccount{ - AccountID: accountToSponsor.Address(), - Sequence: 124, - }, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - &txnbuild.Payment{ - Destination: keypair.MustRandom().Address(), - Amount: "14", - Asset: txnbuild.NativeAsset{}, - }, - }, - BaseFee: 104, - Preconditions: txnbuild.Preconditions{TimeBounds: txnbuild.NewTimeout(10)}, - }) - return tx -} - func TestValidateOptions(t *testing.T) { t.Run("return_error_when_distribution_signature_client_nil", func(t *testing.T) { opts := TransactionServiceOptions{ @@ -141,7 +119,7 @@ func TestSignAndBuildNewFeeBumpTransaction(t *testing.T) { HTTPClient: &MockHTTPClient{}, }) - txStr, _ := buildTestTransaction().Base64() + txStr, _ := BuildTestTransaction().Base64() t.Run("malformed_transaction_string", func(t *testing.T) { feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), "abcd") @@ -231,7 +209,7 @@ func TestSignAndBuildNewFeeBumpTransaction(t *testing.T) { t.Run("horizon_client_sign_stellar_transaction_w_distribition_account_err", func(t *testing.T) { account := keypair.MustRandom() - signedTx := buildTestTransaction() + signedTx := BuildTestTransaction() channelAccountSignatureClient. On("GetAccountPublicKey", context.Background()). Return(account.Address(), nil). @@ -262,7 +240,7 @@ func TestSignAndBuildNewFeeBumpTransaction(t *testing.T) { t.Run("returns_signed_tx", func(t *testing.T) { account := keypair.MustRandom() - signedTx := buildTestTransaction() + signedTx := BuildTestTransaction() testFeeBumpTx, _ := txnbuild.NewFeeBumpTransaction( txnbuild.FeeBumpTransactionParams{ Inner: signedTx, From f10d2398f063347de177d9a85d32aa1ea6216752 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Sat, 21 Sep 2024 16:08:52 -0700 Subject: [PATCH 054/113] removing BuildTestFeeBumpTransaction --- internal/tss/utils/helpers.go | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/internal/tss/utils/helpers.go b/internal/tss/utils/helpers.go index c844a03..04c4f94 100644 --- a/internal/tss/utils/helpers.go +++ b/internal/tss/utils/helpers.go @@ -26,14 +26,3 @@ func BuildTestTransaction() *txnbuild.Transaction { }) return tx } - -func BuildTestFeeBumpTransaction() *txnbuild.FeeBumpTransaction { - - feeBumpTx, _ := txnbuild.NewFeeBumpTransaction( - txnbuild.FeeBumpTransactionParams{ - Inner: BuildTestTransaction(), - FeeAccount: keypair.MustRandom().Address(), - BaseFee: 110, - }) - return feeBumpTx -} From d679abd774b3807bad2a89be3169cc7ac83a17eb Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Sat, 21 Sep 2024 18:34:34 -0700 Subject: [PATCH 055/113] adding to serve.go etc --- cmd/serve.go | 4 ++++ cmd/utils/global_options.go | 44 +++++++++++++++++++++++++++++++++++++ internal/serve/serve.go | 23 ++++++++++++++++++- 3 files changed, 70 insertions(+), 1 deletion(-) diff --git a/cmd/serve.go b/cmd/serve.go index e546036..5e13fc3 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -41,6 +41,10 @@ func (c *serveCmd) Command() *cobra.Command { utils.ErrorHandlerServiceNonJitterChannelWaitBtwnRetriesMSOption(&cfg.ErrorHandlerServiceNonJitterChannelWaitBtwnRetriesMS), utils.ErrorHandlerServiceJitterChannelMaxRetriesOptions(&cfg.ErrorHandlerServiceJitterChannelMaxRetries), utils.ErrorHandlerServiceNonJitterChannelMaxRetriesOption(&cfg.ErrorHandlerServiceNonJitterChannelMaxRetries), + utils.WebhookHandlerServiceChannelMaxBufferSizeOption(&cfg.WebhookHandlerServiceChannelMaxBufferSize), + utils.WebhookHandlerServiceChannelMaxWorkersOptions(&cfg.WebhookHandlerServiceChannelMaxWorkers), + utils.WebhookHandlerServiceChannelMaxRetriesOption(&cfg.WebhookHandlerServiceChannelMaxRetries), + utils.WebhookHandlerServiceChannelMinWaitBtwnRetriesMSOption(&cfg.WebhookHandlerServiceChannelMinWaitBtwnRetriesMS), { Name: "port", Usage: "Port to listen and serve on", diff --git a/cmd/utils/global_options.go b/cmd/utils/global_options.go index a283c10..2b29d93 100644 --- a/cmd/utils/global_options.go +++ b/cmd/utils/global_options.go @@ -232,6 +232,50 @@ func ErrorHandlerServiceNonJitterChannelMaxRetriesOption(configKey *int) *config } } +func WebhookHandlerServiceChannelMaxBufferSizeOption(configKey *int) *config.ConfigOption { + return &config.ConfigOption{ + Name: "webhook-service-channel-max-buffer-size", + Usage: "Set the buffer size of the webhook serive channel.", + OptType: types.Int, + ConfigKey: configKey, + FlagDefault: 100, + Required: true, + } +} + +func WebhookHandlerServiceChannelMaxWorkersOptions(configKey *int) *config.ConfigOption { + return &config.ConfigOption{ + Name: "webhook-service-channel-max-workers", + Usage: "Set the max number of workers for the webhook serive channel.", + OptType: types.Int, + ConfigKey: configKey, + FlagDefault: 10, + Required: true, + } +} + +func WebhookHandlerServiceChannelMaxRetriesOption(configKey *int) *config.ConfigOption { + return &config.ConfigOption{ + Name: "webhook-service-channel-max-retries", + Usage: "Set the max number of times to ping a webhook before quitting.", + OptType: types.Int, + ConfigKey: configKey, + FlagDefault: 3, + Required: true, + } +} + +func WebhookHandlerServiceChannelMinWaitBtwnRetriesMSOption(configKey *int) *config.ConfigOption { + return &config.ConfigOption{ + Name: "webhook-service-channel-min-wait-between-retries", + Usage: "The minumum amout of time to wait before repining the webhook url", + OptType: types.Int, + ConfigKey: configKey, + FlagDefault: 10, + Required: true, + } +} + func AWSOptions(awsRegionConfigKey *string, kmsKeyARN *string, required bool) config.ConfigOptions { awsOpts := config.ConfigOptions{ { diff --git a/internal/serve/serve.go b/internal/serve/serve.go index 6c302d0..9791848 100644 --- a/internal/serve/serve.go +++ b/internal/serve/serve.go @@ -78,6 +78,10 @@ type Configs struct { ErrorHandlerServiceNonJitterChannelWaitBtwnRetriesMS int ErrorHandlerServiceJitterChannelMaxRetries int ErrorHandlerServiceNonJitterChannelMaxRetries int + WebhookHandlerServiceChannelMaxBufferSize int + WebhookHandlerServiceChannelMaxWorkers int + WebhookHandlerServiceChannelMaxRetries int + WebhookHandlerServiceChannelMinWaitBtwnRetriesMS int } type handlerDeps struct { @@ -96,6 +100,7 @@ type handlerDeps struct { // TSS ErrorHandlerServiceJitterChannel tss.Channel ErrorHandlerServiceNonJitterChannel tss.Channel + WebhookHandlerServiceChannel tss.Channel } func Serve(cfg Configs) error { @@ -115,6 +120,7 @@ func Serve(cfg Configs) error { log.Info("Stopping Wallet Backend server") deps.ErrorHandlerServiceJitterChannel.Stop() deps.ErrorHandlerServiceNonJitterChannel.Stop() + deps.WebhookHandlerServiceChannel.Stop() }, }) @@ -179,12 +185,15 @@ func initHandlerDeps(cfg Configs) (handlerDeps, error) { go ensureChannelAccounts(channelAccountService, int64(cfg.NumberOfChannelAccounts)) // TSS + httpClient := http.Client{Timeout: time.Duration(30 * time.Second)} + txServiceOpts := tssutils.TransactionServiceOptions{ DistributionAccountSignatureClient: cfg.DistributionAccountSignatureClient, ChannelAccountSignatureClient: cfg.ChannelAccountSignatureClient, HorizonClient: &horizonClient, RPCURL: cfg.RPCURL, BaseFee: int64(cfg.BaseFee), // Reuse horizon base fee for RPC?? + HTTPClient: &httpClient, } tssTxService, err := tssutils.NewTransactionService(txServiceOpts) @@ -221,7 +230,18 @@ func initHandlerDeps(cfg Configs) (handlerDeps, error) { NonJitterChannel: nonJitterChannel, }) - webhookHandlerService := tssservices.NewWebhookHandlerService(nil) + httpClient = http.Client{Timeout: time.Duration(30 * time.Second)} + webhookHandlerServiceChannelOps := tsschannel.WebhookHandlerServiceChannelConfigs{ + HTTPClient: &httpClient, + MaxBufferSize: cfg.WebhookHandlerServiceChannelMaxBufferSize, + MaxWorkers: cfg.WebhookHandlerServiceChannelMaxWorkers, + MaxRetries: cfg.WebhookHandlerServiceChannelMaxRetries, + MinWaitBtwnRetriesMS: cfg.WebhookHandlerServiceChannelMinWaitBtwnRetriesMS, + } + + webhookCallerServiceChannel := tsschannel.NewWebhookHandlerServiceChannel(webhookHandlerServiceChannelOps) + + webhookHandlerService := tssservices.NewWebhookHandlerService(webhookCallerServiceChannel) router := tssrouter.NewRouter(tssrouter.RouterConfigs{ ErrorHandlerService: errHandlerService, @@ -241,6 +261,7 @@ func initHandlerDeps(cfg Configs) (handlerDeps, error) { AppTracker: cfg.AppTracker, ErrorHandlerServiceJitterChannel: jitterChannel, ErrorHandlerServiceNonJitterChannel: nonJitterChannel, + WebhookHandlerServiceChannel: webhookCallerServiceChannel, }, nil } From db8714d9d7916ee3998a77b284f3e7138a4bf4e0 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Sat, 21 Sep 2024 19:00:39 -0700 Subject: [PATCH 056/113] casing --- cmd/serve.go | 2 +- internal/serve/serve.go | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/cmd/serve.go b/cmd/serve.go index 02bb189..abed44f 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -29,7 +29,7 @@ func (c *serveCmd) Command() *cobra.Command { utils.NetworkPassphraseOption(&cfg.NetworkPassphrase), utils.BaseFeeOption(&cfg.BaseFee), utils.HorizonClientURLOption(&cfg.HorizonClientURL), - utils.RPCURLOption(&cfg.RpcUrl), + utils.RPCURLOption(&cfg.RPCURL), utils.RPCCallerServiceChannelBufferSizeOption(&cfg.RPCCallerServiceChannelBufferSize), utils.RPCCallerServiceMaxWorkersOption(&cfg.RPCCallerServiceChannelMaxWorkers), utils.ChannelAccountEncryptionPassphraseOption(&cfg.EncryptionPassphrase), diff --git a/internal/serve/serve.go b/internal/serve/serve.go index 4f448b3..252cfb2 100644 --- a/internal/serve/serve.go +++ b/internal/serve/serve.go @@ -65,7 +65,7 @@ type Configs struct { DistributionAccountSignatureClient signing.SignatureClient ChannelAccountSignatureClient signing.SignatureClient // TSS - RpcUrl string + RPCURL string RPCCallerServiceChannelBufferSize int RPCCallerServiceChannelMaxWorkers int // Error Tracker @@ -84,8 +84,8 @@ type handlerDeps struct { AccountSponsorshipService services.AccountSponsorshipService PaymentService services.PaymentService // TSS - RpcCallerServiceChannel tss.Channel - RpcCallerService tssservices.Service + RPCCallerServiceChannel tss.Channel + RPCCallerService tssservices.Service AppTracker apptracker.AppTracker } @@ -104,7 +104,7 @@ func Serve(cfg Configs) error { }, OnStopping: func() { log.Info("Stopping Wallet Backend server") - deps.RpcCallerServiceChannel.Stop() + deps.RPCCallerServiceChannel.Stop() }, }) @@ -174,7 +174,7 @@ func initHandlerDeps(cfg Configs) (handlerDeps, error) { DistributionAccountSignatureClient: cfg.DistributionAccountSignatureClient, ChannelAccountSignatureClient: cfg.ChannelAccountSignatureClient, HorizonClient: &horizonClient, - RPCURL: cfg.RpcUrl, + RPCURL: cfg.RPCURL, BaseFee: int64(cfg.BaseFee), // Reuse horizon base fee for RPC?? HTTPClient: &httpClient, } @@ -210,8 +210,8 @@ func initHandlerDeps(cfg Configs) (handlerDeps, error) { PaymentService: paymentService, AppTracker: cfg.AppTracker, // TSS - RpcCallerServiceChannel: rpcCallerServiceChannel, - RpcCallerService: rpcCallerService, + RPCCallerServiceChannel: rpcCallerServiceChannel, + RPCCallerService: rpcCallerService, }, nil } From 2c0d060c1cabe8e987caead4931fa090af21be57 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Sun, 22 Sep 2024 17:25:49 -0700 Subject: [PATCH 057/113] better test for Send --- .../channels/rpc_caller_service_channel.go | 2 +- .../rpc_caller_service_channel_test.go | 22 ++++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/internal/tss/channels/rpc_caller_service_channel.go b/internal/tss/channels/rpc_caller_service_channel.go index 9cb4f47..33300c3 100644 --- a/internal/tss/channels/rpc_caller_service_channel.go +++ b/internal/tss/channels/rpc_caller_service_channel.go @@ -29,7 +29,7 @@ type rpcCallerServicePool struct { Router router.Router } -func NewRPCCallerServiceChannel(cfg RPCCallerServiceChannelConfigs) tss.Channel { +func NewRPCCallerServiceChannel(cfg RPCCallerServiceChannelConfigs) *rpcCallerServicePool { pool := pond.New(cfg.MaxBufferSize, cfg.MaxWorkers, pond.Strategy(pond.Balanced())) return &rpcCallerServicePool{ Pool: pool, diff --git a/internal/tss/channels/rpc_caller_service_channel_test.go b/internal/tss/channels/rpc_caller_service_channel_test.go index 276080a..797b56d 100644 --- a/internal/tss/channels/rpc_caller_service_channel_test.go +++ b/internal/tss/channels/rpc_caller_service_channel_test.go @@ -36,9 +36,23 @@ func TestSend(t *testing.T) { payload.WebhookURL = "www.stellar.com" payload.TransactionHash = "hash" payload.TransactionXDR = "xdr" + networkPass := "passphrase" + + feeBumpTx := utils.BuildTestFeeBumpTransaction() + feeBumpTxXDR, _ := feeBumpTx.Base64() + sendResp := tss.RPCSendTxResponse{} + sendResp.Code.OtherCodes = tss.RPCFailCode txServiceMock. On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(nil, errors.New("signing failed")) + Return(feeBumpTx, nil). + Once(). + On("NetworkPassphrase"). + Return(networkPass). + Once(). + On("SendTransaction", feeBumpTxXDR). + Return(sendResp, errors.New("RPC Fail")). + Once() + channel.Send(payload) channel.Stop() @@ -46,6 +60,12 @@ func TestSend(t *testing.T) { err = dbConnectionPool.GetContext(context.Background(), &status, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) require.NoError(t, err) assert.Equal(t, status, string(tss.NewStatus)) + + var tryStatus int + feeBumpTxHash, _ := feeBumpTx.HashHex(networkPass) + err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) + require.NoError(t, err) + assert.Equal(t, int(tss.RPCFailCode), tryStatus) } func TestReceive(t *testing.T) { From 9d1266bbb5d297bb31760ca5e89b6a73e8593a4e Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Sun, 22 Sep 2024 19:14:49 -0700 Subject: [PATCH 058/113] resolving merge conflicts --- cmd/utils/global_options.go | 53 +++++++++++++++---------------------- internal/serve/serve.go | 11 +++----- 2 files changed, 25 insertions(+), 39 deletions(-) diff --git a/cmd/utils/global_options.go b/cmd/utils/global_options.go index 7056919..6719d9d 100644 --- a/cmd/utils/global_options.go +++ b/cmd/utils/global_options.go @@ -67,38 +67,6 @@ func HorizonClientURLOption(configKey *string) *config.ConfigOption { } } -func RPCURLOption(configKey *string) *config.ConfigOption { - return &config.ConfigOption{ - Name: "rpc-url", - Usage: "The URL of the RPC Server.", - OptType: types.String, - ConfigKey: configKey, - FlagDefault: "localhost:8080", - Required: true, - } -} - -func RPCCallerServiceChannelBufferSizeOption(configKey *int) *config.ConfigOption { - return &config.ConfigOption{ - Name: "tss-rpc-caller-service-channel-buffer-size", - Usage: "Set the buffer size for TSS RPC Caller Service channel.", - OptType: types.Int, - ConfigKey: configKey, - FlagDefault: 1000, - } -} - -func RPCCallerServiceMaxWorkersOption(configKey *int) *config.ConfigOption { - return &config.ConfigOption{ - Name: "tss-rpc-caller-service-channel-max-workers", - Usage: "Set the maximum number of workers for TSS RPC Caller Service channel.", - OptType: types.Int, - ConfigKey: configKey, - FlagDefault: 100, - } - -} - func ChannelAccountEncryptionPassphraseOption(configKey *string) *config.ConfigOption { return &config.ConfigOption{ Name: "channel-account-encryption-passphrase", @@ -174,6 +142,27 @@ func RPCURLOption(configKey *string) *config.ConfigOption { } } +func RPCCallerServiceChannelBufferSizeOption(configKey *int) *config.ConfigOption { + return &config.ConfigOption{ + Name: "tss-rpc-caller-service-channel-buffer-size", + Usage: "Set the buffer size for TSS RPC Caller Service channel.", + OptType: types.Int, + ConfigKey: configKey, + FlagDefault: 1000, + } +} + +func RPCCallerServiceMaxWorkersOption(configKey *int) *config.ConfigOption { + return &config.ConfigOption{ + Name: "tss-rpc-caller-service-channel-max-workers", + Usage: "Set the maximum number of workers for TSS RPC Caller Service channel.", + OptType: types.Int, + ConfigKey: configKey, + FlagDefault: 100, + } + +} + func ErrorHandlerServiceJitterChannelBufferSizeOption(configKey *int) *config.ConfigOption { return &config.ConfigOption{ Name: "error-handler-service-jitter-channel-buffer-size", diff --git a/internal/serve/serve.go b/internal/serve/serve.go index 38d5fc9..d7ec190 100644 --- a/internal/serve/serve.go +++ b/internal/serve/serve.go @@ -64,15 +64,10 @@ type Configs struct { HorizonClientURL string DistributionAccountSignatureClient signing.SignatureClient ChannelAccountSignatureClient signing.SignatureClient - // TSS - RPCURL string - RPCCallerServiceChannelBufferSize int - RPCCallerServiceChannelMaxWorkers int - // Error Tracker - AppTracker apptracker.AppTracker - // TSS RPCURL string + RPCCallerServiceChannelBufferSize int + RPCCallerServiceChannelMaxWorkers int ErrorHandlerServiceJitterChannelBufferSize int ErrorHandlerServiceJitterChannelMaxWorkers int ErrorHandlerServiceNonJitterChannelBufferSize int @@ -81,6 +76,8 @@ type Configs struct { ErrorHandlerServiceNonJitterChannelWaitBtwnRetriesMS int ErrorHandlerServiceJitterChannelMaxRetries int ErrorHandlerServiceNonJitterChannelMaxRetries int + // Error Tracker + AppTracker apptracker.AppTracker } type handlerDeps struct { From a196677a27a28896e200d0b60a6b3c2b16eab147 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Mon, 23 Sep 2024 11:06:40 -0700 Subject: [PATCH 059/113] removing mockSleep --- .../error_handler_service_jitter_channel.go | 2 +- ...rror_handler_service_non_jitter_channel.go | 2 +- ...handler_service_non_jitter_channel_test.go | 29 ------------------- ...ror_service_handler_jitter_channel_test.go | 29 ------------------- internal/tss/channels/types.go | 4 --- 5 files changed, 2 insertions(+), 64 deletions(-) diff --git a/internal/tss/channels/error_handler_service_jitter_channel.go b/internal/tss/channels/error_handler_service_jitter_channel.go index 09cd8a0..a13c442 100644 --- a/internal/tss/channels/error_handler_service_jitter_channel.go +++ b/internal/tss/channels/error_handler_service_jitter_channel.go @@ -61,7 +61,7 @@ func (p *rpcErrorHandlerServiceJitterPool) Receive(payload tss.Payload) { var i int for i = 0; i < p.MaxRetries; i++ { currentBackoff := p.MinWaitBtwnRetriesMS * (1 << i) - sleep(jitter(time.Duration(currentBackoff)) * time.Microsecond) + time.Sleep(jitter(time.Duration(currentBackoff)) * time.Microsecond) rpcSendResp, err := BuildAndSubmitTransaction(ctx, "ErrorHandlerServiceJitterChannel", payload, p.Store, p.TxService) if err != nil { log.Errorf(err.Error()) diff --git a/internal/tss/channels/error_handler_service_non_jitter_channel.go b/internal/tss/channels/error_handler_service_non_jitter_channel.go index d572f00..16508b0 100644 --- a/internal/tss/channels/error_handler_service_non_jitter_channel.go +++ b/internal/tss/channels/error_handler_service_non_jitter_channel.go @@ -53,7 +53,7 @@ func (p *rpcErrorHandlerServiceNonJitterPool) Receive(payload tss.Payload) { ctx := context.Background() var i int for i = 0; i < p.MaxRetries; i++ { - sleep(time.Duration(p.WaitBtwnRetriesMS) * time.Microsecond) + time.Sleep(time.Duration(p.WaitBtwnRetriesMS) * time.Microsecond) rpcSendResp, err := BuildAndSubmitTransaction(ctx, "ErrorHandlerServiceNonJitterChannel", payload, p.Store, p.TxService) if err != nil { log.Errorf(err.Error()) diff --git a/internal/tss/channels/error_handler_service_non_jitter_channel_test.go b/internal/tss/channels/error_handler_service_non_jitter_channel_test.go index 083a7a6..51eae3d 100644 --- a/internal/tss/channels/error_handler_service_non_jitter_channel_test.go +++ b/internal/tss/channels/error_handler_service_non_jitter_channel_test.go @@ -4,7 +4,6 @@ import ( "context" "errors" "testing" - "time" "github.com/stellar/go/xdr" "github.com/stellar/wallet-backend/internal/db" @@ -75,14 +74,6 @@ func TestNonJitterReceive(t *testing.T) { } channel := NewErrorHandlerServiceNonJitterChannel(cfg) - // mock out the sleep function (time.Sleep) so we can check the args it was called with - mockSleep := MockSleep{} - defer mockSleep.AssertExpectations(t) - sleep = mockSleep.Sleep - defer func() { - sleep = time.Sleep - }() - mockRouter := router.MockRouter{} defer mockRouter.AssertExpectations(t) channel.SetRouter(&mockRouter) @@ -103,11 +94,6 @@ func TestNonJitterReceive(t *testing.T) { Return(nil, errors.New("sign tx failed")). Once() - mockSleep. - On("Sleep", time.Duration(time.Duration(channel.WaitBtwnRetriesMS)*time.Microsecond)). - Return(). - Once() - channel.Receive(payload) var txStatus string @@ -133,11 +119,6 @@ func TestNonJitterReceive(t *testing.T) { Return(sendResp, nil). Once() - mockSleep. - On("Sleep", time.Duration(time.Duration(channel.WaitBtwnRetriesMS)*time.Microsecond)). - Return(). - Once() - mockRouter. On("Route", mock.AnythingOfType("tss.Payload")). Return(). @@ -191,11 +172,6 @@ func TestNonJitterReceive(t *testing.T) { Return(). Once() - mockSleep. - On("Sleep", time.Duration(time.Duration(channel.WaitBtwnRetriesMS)*time.Microsecond)). - Return(). - Twice() - channel.Receive(payload) var txStatus string @@ -233,11 +209,6 @@ func TestNonJitterReceive(t *testing.T) { Return(). Once() - mockSleep. - On("Sleep", time.Duration(time.Duration(channel.WaitBtwnRetriesMS)*time.Microsecond)). - Return(). - Times(3) - channel.Receive(payload) var txStatus string diff --git a/internal/tss/channels/error_service_handler_jitter_channel_test.go b/internal/tss/channels/error_service_handler_jitter_channel_test.go index f0023c3..bb4077d 100644 --- a/internal/tss/channels/error_service_handler_jitter_channel_test.go +++ b/internal/tss/channels/error_service_handler_jitter_channel_test.go @@ -4,7 +4,6 @@ import ( "context" "errors" "testing" - "time" "github.com/stellar/go/xdr" "github.com/stellar/wallet-backend/internal/db" @@ -75,14 +74,6 @@ func TestJitterReceive(t *testing.T) { } channel := NewErrorHandlerServiceJitterChannel(cfg) - // mock out the sleep function (time.Sleep) so we can check the args it was called with - mockSleep := MockSleep{} - defer mockSleep.AssertExpectations(t) - sleep = mockSleep.Sleep - defer func() { - sleep = time.Sleep - }() - mockRouter := router.MockRouter{} defer mockRouter.AssertExpectations(t) channel.SetRouter(&mockRouter) @@ -103,11 +94,6 @@ func TestJitterReceive(t *testing.T) { Return(nil, errors.New("sign tx failed")). Once() - mockSleep. - On("Sleep", mock.AnythingOfType("time.Duration")). - Return(). - Once() - channel.Receive(payload) var txStatus string @@ -133,11 +119,6 @@ func TestJitterReceive(t *testing.T) { Return(sendResp, nil). Once() - mockSleep. - On("Sleep", mock.AnythingOfType("time.Duration")). - Return(). - Once() - mockRouter. On("Route", mock.AnythingOfType("tss.Payload")). Return(). @@ -191,11 +172,6 @@ func TestJitterReceive(t *testing.T) { Return(). Once() - mockSleep. - On("Sleep", mock.AnythingOfType("time.Duration")). - Return(). - Twice() - channel.Receive(payload) var txStatus string @@ -233,11 +209,6 @@ func TestJitterReceive(t *testing.T) { Return(). Once() - mockSleep. - On("Sleep", mock.AnythingOfType("time.Duration")). - Return(). - Times(3) - channel.Receive(payload) var txStatus string diff --git a/internal/tss/channels/types.go b/internal/tss/channels/types.go index 98bbb17..65ab5e9 100644 --- a/internal/tss/channels/types.go +++ b/internal/tss/channels/types.go @@ -1,9 +1,5 @@ package channels -import "time" - -var sleep = time.Sleep - type WorkerPool interface { Submit(task func()) StopAndWait() From 875b1dda2490b68a65c9ad88b781987eb0dc7663 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Mon, 23 Sep 2024 11:08:26 -0700 Subject: [PATCH 060/113] delete channels/mocks.go --- internal/tss/channels/mocks.go | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 internal/tss/channels/mocks.go diff --git a/internal/tss/channels/mocks.go b/internal/tss/channels/mocks.go deleted file mode 100644 index d6ddc0b..0000000 --- a/internal/tss/channels/mocks.go +++ /dev/null @@ -1,15 +0,0 @@ -package channels - -import ( - "time" - - "github.com/stretchr/testify/mock" -) - -type MockSleep struct { - mock.Mock -} - -func (m *MockSleep) Sleep(d time.Duration) { - m.Called(d) -} From aff5ed427a3ce9a4c682b240797fcf9072159fe6 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Mon, 23 Sep 2024 11:14:34 -0700 Subject: [PATCH 061/113] sleep -> time.Sleep --- internal/tss/channels/webhook_handler_service_channel.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/tss/channels/webhook_handler_service_channel.go b/internal/tss/channels/webhook_handler_service_channel.go index b833992..49ddde5 100644 --- a/internal/tss/channels/webhook_handler_service_channel.go +++ b/internal/tss/channels/webhook_handler_service_channel.go @@ -65,7 +65,7 @@ func (p *webhookHandlerServicePool) Receive(payload tss.Payload) { return } currentBackoff := p.MinWaitBtwnRetriesMS * (1 << i) - sleep(jitter(time.Duration(currentBackoff)) * time.Microsecond) + time.Sleep(jitter(time.Duration(currentBackoff)) * time.Microsecond) } } From 637aeafac693c8525ceda13e9c72f7d767f37d70 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Wed, 25 Sep 2024 20:01:45 -0700 Subject: [PATCH 062/113] incorporating Daniel's changes + comments --- go.mod | 2 +- internal/entities/rpc.go | 58 ++ internal/serve/serve.go | 39 +- internal/services/mocks.go | 26 + internal/services/rpc_service.go | 101 +++ internal/services/rpc_service_bu.go | 133 ++++ internal/services/rpc_service_test_bu.go | 332 ++++++++++ .../channels/rpc_caller_service_channel.go | 35 +- .../rpc_caller_service_channel_test.go | 130 +++- internal/tss/channels/utils.go | 46 -- internal/tss/channels/utils_test.go | 143 ---- internal/tss/mocks.go | 19 + internal/tss/router/mocks.go | 5 +- internal/tss/router/router.go | 68 +- internal/tss/router/router_test.go | 129 +++- .../tss/services/error_handler_service.go | 19 - internal/tss/services/mocks.go | 54 +- internal/tss/services/rpc_caller_service.go | 21 - internal/tss/services/transaction_manager.go | 76 +++ .../tss/services/transaction_manager_test.go | 195 ++++++ .../transaction_service.go | 117 +--- .../tss/services/transaction_service_test.go | 236 +++++++ internal/tss/services/types.go | 7 - .../tss/services/webhook_handler_service.go | 19 - internal/tss/store/store.go | 2 +- internal/tss/store/store_test.go | 9 +- internal/tss/types.go | 141 ++-- internal/tss/types_bu.go | 98 +++ internal/tss/utils/mocks.go | 51 -- .../tss/utils/transaction_service_test.go | 613 ------------------ internal/utils/http_client.go | 21 + 31 files changed, 1748 insertions(+), 1197 deletions(-) create mode 100644 internal/entities/rpc.go create mode 100644 internal/services/mocks.go create mode 100644 internal/services/rpc_service.go create mode 100644 internal/services/rpc_service_bu.go create mode 100644 internal/services/rpc_service_test_bu.go delete mode 100644 internal/tss/channels/utils.go delete mode 100644 internal/tss/channels/utils_test.go create mode 100644 internal/tss/mocks.go delete mode 100644 internal/tss/services/error_handler_service.go delete mode 100644 internal/tss/services/rpc_caller_service.go create mode 100644 internal/tss/services/transaction_manager.go create mode 100644 internal/tss/services/transaction_manager_test.go rename internal/tss/{utils => services}/transaction_service.go (50%) create mode 100644 internal/tss/services/transaction_service_test.go delete mode 100644 internal/tss/services/types.go delete mode 100644 internal/tss/services/webhook_handler_service.go create mode 100644 internal/tss/types_bu.go delete mode 100644 internal/tss/utils/mocks.go delete mode 100644 internal/tss/utils/transaction_service_test.go create mode 100644 internal/utils/http_client.go diff --git a/go.mod b/go.mod index 3ddd30a..063511e 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,6 @@ require ( github.com/stellar/go v0.0.0-20240416222646-fd107948e6c4 github.com/stellar/go-xdr v0.0.0-20231122183749-b53fb00bcac2 github.com/stretchr/testify v1.9.0 - golang.org/x/net v0.23.0 golang.org/x/term v0.18.0 ) @@ -91,6 +90,7 @@ require ( golang.org/x/crypto v0.21.0 // indirect golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect golang.org/x/mod v0.13.0 // indirect + golang.org/x/net v0.23.0 // indirect golang.org/x/oauth2 v0.16.0 // indirect golang.org/x/sync v0.6.0 // indirect golang.org/x/sys v0.18.0 // indirect diff --git a/internal/entities/rpc.go b/internal/entities/rpc.go new file mode 100644 index 0000000..eb252bb --- /dev/null +++ b/internal/entities/rpc.go @@ -0,0 +1,58 @@ +package entities + +import ( + "encoding/json" +) + +type RPCStatus string + +const ( + // sendTransaction statuses + PendingStatus RPCStatus = "PENDING" + DuplicateStatus RPCStatus = "DUPLICATE" + TryAgainLaterStatus RPCStatus = "TRY_AGAIN_LATER" + ErrorStatus RPCStatus = "ERROR" + // getTransaction statuses + NotFoundStatus RPCStatus = "NOT_FOUND" + FailedStatus RPCStatus = "FAILED" + SuccessStatus RPCStatus = "SUCCESS" +) + +type RPCEntry struct { + Key string `json:"key"` + XDR string `json:"xdr"` + LastModifiedLedgerSeq int64 `json:"lastModifiedLedgerSeq"` +} + +type RPCResponse struct { + Result json.RawMessage `json:"result"` + JSONRPC string `json:"jsonrpc"` + ID int64 `json:"id"` +} + +type RPCGetLedgerEntriesResult struct { + Entries []RPCEntry `json:"entries"` +} + +type RPCGetTransactionResult struct { + Status string `json:"status"` + LatestLedger int64 `json:"latestLedger"` + LatestLedgerCloseTime string `json:"latestLedgerCloseTime"` + OldestLedger string `json:"oldestLedger"` + OldestLedgerCloseTime string `json:"oldestLedgerCloseTime"` + ApplicationOrder string `json:"applicationOrder"` + EnvelopeXDR string `json:"envelopeXdr"` + ResultXDR string `json:"resultXdr"` + ResultMetaXDR string `json:"resultMetaXdr"` + Ledger string `json:"ledger"` + CreatedAt string `json:"createdAt"` + ErrorResultXDR string `json:"errorResultXdr"` +} + +type RPCSendTransactionResult struct { + Status string `json:"status"` + LatestLedger int64 `json:"latestLedger"` + LatestLedgerCloseTime string `json:"latestLedgerCloseTime"` + Hash string `json:"hash"` + ErrorResultXDR string `json:"errorResultXdr"` +} diff --git a/internal/serve/serve.go b/internal/serve/serve.go index 252cfb2..6ef3670 100644 --- a/internal/serve/serve.go +++ b/internal/serve/serve.go @@ -30,7 +30,6 @@ import ( tssrouter "github.com/stellar/wallet-backend/internal/tss/router" tssservices "github.com/stellar/wallet-backend/internal/tss/services" tssstore "github.com/stellar/wallet-backend/internal/tss/store" - tssutils "github.com/stellar/wallet-backend/internal/tss/utils" ) // NOTE: perhaps move this to a environment variable. @@ -85,7 +84,7 @@ type handlerDeps struct { PaymentService services.PaymentService // TSS RPCCallerServiceChannel tss.Channel - RPCCallerService tssservices.Service + TSSRouter tssrouter.Router AppTracker apptracker.AppTracker } @@ -169,37 +168,45 @@ func initHandlerDeps(cfg Configs) (handlerDeps, error) { go ensureChannelAccounts(channelAccountService, int64(cfg.NumberOfChannelAccounts)) // TSS - httpClient := http.Client{Timeout: time.Duration(30 * time.Second)} - txServiceOpts := tssutils.TransactionServiceOptions{ + txServiceOpts := tssservices.TransactionServiceOptions{ DistributionAccountSignatureClient: cfg.DistributionAccountSignatureClient, ChannelAccountSignatureClient: cfg.ChannelAccountSignatureClient, HorizonClient: &horizonClient, - RPCURL: cfg.RPCURL, BaseFee: int64(cfg.BaseFee), // Reuse horizon base fee for RPC?? - HTTPClient: &httpClient, } - tssTxService, err := tssutils.NewTransactionService(txServiceOpts) + tssTxService, err := tssservices.NewTransactionService(txServiceOpts) if err != nil { return handlerDeps{}, fmt.Errorf("instantiating tss transaction service: %w", err) } + httpClient := http.Client{Timeout: time.Duration(30 * time.Second)} + rpcService, err := services.NewRPCService(cfg.RPCURL, &httpClient) + if err != nil { + return handlerDeps{}, fmt.Errorf("instantiating rpc service: %w", err) + } // re-use same context as above?? store := tssstore.NewStore(dbConnectionPool) - errorHandlerService := tssservices.NewErrorHandlerService(nil) - webhookHandlerService := tssservices.NewWebhookHandlerService(nil) - router := tssrouter.NewRouter(tssrouter.RouterConfigs{ - ErrorHandlerService: errorHandlerService, - WebhookHandlerService: webhookHandlerService, + txManager := tssservices.NewTransactionManager(tssservices.TransactionManagerConfigs{ + TxService: tssTxService, + RPCService: rpcService, + Store: store, }) tssChannelConfigs := tsschannel.RPCCallerServiceChannelConfigs{ + TxManager: txManager, Store: store, - TxService: tssTxService, - Router: router, MaxBufferSize: cfg.RPCCallerServiceChannelBufferSize, MaxWorkers: cfg.RPCCallerServiceChannelMaxWorkers, } rpcCallerServiceChannel := tsschannel.NewRPCCallerServiceChannel(tssChannelConfigs) - rpcCallerService := tssservices.NewRPCCallerService(rpcCallerServiceChannel) + + router := tssrouter.NewRouter(tssrouter.RouterConfigs{ + RPCCallerChannel: rpcCallerServiceChannel, + ErrorJitterChannel: nil, + ErrorNonJitterChannel: nil, + WebhookChannel: nil, + }) + + rpcCallerServiceChannel.SetRouter(router) return handlerDeps{ Models: models, @@ -211,7 +218,7 @@ func initHandlerDeps(cfg Configs) (handlerDeps, error) { AppTracker: cfg.AppTracker, // TSS RPCCallerServiceChannel: rpcCallerServiceChannel, - RPCCallerService: rpcCallerService, + TSSRouter: router, }, nil } diff --git a/internal/services/mocks.go b/internal/services/mocks.go new file mode 100644 index 0000000..255c955 --- /dev/null +++ b/internal/services/mocks.go @@ -0,0 +1,26 @@ +package services + +import ( + "github.com/stellar/wallet-backend/internal/entities" + "github.com/stretchr/testify/mock" +) + +type RPCServiceMock struct { + mock.Mock +} + +var _ RPCService = (*RPCServiceMock)(nil) + +func (r *RPCServiceMock) SendTransaction(transactionXdr string) (entities.RPCSendTransactionResult, error) { + args := r.Called(transactionXdr) + return args.Get(0).(entities.RPCSendTransactionResult), args.Error(1) +} + +func (r *RPCServiceMock) GetTransaction(transactionHash string) (entities.RPCGetTransactionResult, error) { + args := r.Called(transactionHash) + return args.Get(0).(entities.RPCGetTransactionResult), args.Error(1) +} + +type TransactionManagerMock struct { + mock.Mock +} diff --git a/internal/services/rpc_service.go b/internal/services/rpc_service.go new file mode 100644 index 0000000..da10b93 --- /dev/null +++ b/internal/services/rpc_service.go @@ -0,0 +1,101 @@ +package services + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + + "github.com/stellar/wallet-backend/internal/entities" + "github.com/stellar/wallet-backend/internal/utils" +) + +type RPCService interface { + GetTransaction(transactionHash string) (entities.RPCGetTransactionResult, error) + SendTransaction(transactionXDR string) (entities.RPCSendTransactionResult, error) +} + +type rpcService struct { + rpcURL string + httpClient utils.HTTPClient +} + +var _ RPCService = (*rpcService)(nil) + +func NewRPCService(rpcURL string, httpClient utils.HTTPClient) (*rpcService, error) { + if rpcURL == "" { + return nil, errors.New("rpcURL cannot be nil") + } + if httpClient == nil { + return nil, errors.New("httpClient cannot be nil") + } + + return &rpcService{ + rpcURL: rpcURL, + httpClient: httpClient, + }, nil +} + +func (r *rpcService) GetTransaction(transactionHash string) (entities.RPCGetTransactionResult, error) { + resultBytes, err := r.sendRPCRequest("getTransaction", map[string]string{"hash": transactionHash}) + if err != nil { + return entities.RPCGetTransactionResult{}, fmt.Errorf("sending getTransaction request: %w", err) + } + + var result entities.RPCGetTransactionResult + err = json.Unmarshal(resultBytes, &result) + if err != nil { + return entities.RPCGetTransactionResult{}, fmt.Errorf("parsing getTransaction result JSON: %w", err) + } + + return result, nil +} + +func (r *rpcService) SendTransaction(transactionXDR string) (entities.RPCSendTransactionResult, error) { + resultBytes, err := r.sendRPCRequest("sendTransaction", map[string]string{"transaction": transactionXDR}) + if err != nil { + return entities.RPCSendTransactionResult{}, fmt.Errorf("sending sendTransaction request: %w", err) + } + + var result entities.RPCSendTransactionResult + err = json.Unmarshal(resultBytes, &result) + if err != nil { + return entities.RPCSendTransactionResult{}, fmt.Errorf("parsing sendTransaction result JSON: %w", err) + } + + return result, nil +} + +func (r *rpcService) sendRPCRequest(method string, params map[string]string) (json.RawMessage, error) { + payload := map[string]interface{}{ + "jsonrpc": "2.0", + "id": 1, + "method": method, + "params": params, + } + jsonData, err := json.Marshal(payload) + + if err != nil { + return nil, fmt.Errorf("marshaling payload") + } + + resp, err := r.httpClient.Post(r.rpcURL, "application/json", bytes.NewBuffer(jsonData)) + if err != nil { + return nil, fmt.Errorf("sending POST request to RPC: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("unmarshaling RPC response: %w", err) + } + + var res entities.RPCResponse + err = json.Unmarshal(body, &res) + if err != nil { + return nil, fmt.Errorf("parsing RPC response JSON: %w", err) + } + + return res.Result, nil +} diff --git a/internal/services/rpc_service_bu.go b/internal/services/rpc_service_bu.go new file mode 100644 index 0000000..bfdcd2e --- /dev/null +++ b/internal/services/rpc_service_bu.go @@ -0,0 +1,133 @@ +package services + +/* +package services + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "strconv" + + xdr3 "github.com/stellar/go-xdr/xdr3" + "github.com/stellar/go/xdr" + "github.com/stellar/wallet-backend/internal/tss" +) + +type HTTPClient interface { + Post(url string, t string, body io.Reader) (resp *http.Response, err error) +} + +type RPCService interface { + SendTransaction(transactionXdr string) (tss.RPCSendTxResponse, error) + GetTransaction(transactionHash string) (tss.RPCGetIngestTxResponse, error) +} + +type RPCServiceOptions struct { + RPCURL string + HTTPClient HTTPClient +} + +type rpcService struct { + RPCURL string + HTTPClient HTTPClient +} + +var _ RPCService = (*rpcService)(nil) + +func NewRPCService(cfg RPCServiceOptions) *rpcService { + return &rpcService{ + RPCURL: cfg.RPCURL, + HTTPClient: cfg.HTTPClient, + } +} + +func (r *rpcService) GetTransaction(transactionHash string) (tss.RPCGetIngestTxResponse, error) { + rpcResponse, err := r.sendRPCRequest("getTransaction", map[string]string{"hash": transactionHash}) + if err != nil { + return tss.RPCGetIngestTxResponse{Status: tss.ErrorStatus}, fmt.Errorf("RPC Fail: %s", err.Error()) + } + getIngestTxResponse := tss.RPCGetIngestTxResponse{ + Status: tss.RPCTXStatus(rpcResponse.RPCResult.Status), + EnvelopeXDR: rpcResponse.RPCResult.EnvelopeXDR, + ResultXDR: rpcResponse.RPCResult.ResultXDR, + } + if getIngestTxResponse.Status != tss.NotFoundStatus { + getIngestTxResponse.CreatedAt, err = strconv.ParseInt(rpcResponse.RPCResult.CreatedAt, 10, 64) + if err != nil { + return tss.RPCGetIngestTxResponse{Status: tss.ErrorStatus}, fmt.Errorf("unable to parse createAt: %w", err) + } + } + return getIngestTxResponse, nil +} + +func (r *rpcService) SendTransaction(transactionXdr string) (tss.RPCSendTxResponse, error) { + rpcResponse, err := r.sendRPCRequest("sendTransaction", map[string]string{"transaction": transactionXdr}) + sendTxResponse := tss.RPCSendTxResponse{} + sendTxResponse.TransactionXDR = transactionXdr + if err != nil { + sendTxResponse.Code.OtherCodes = tss.RPCFailCode + return sendTxResponse, fmt.Errorf("RPC fail: %w", err) + } + sendTxResponse.Status = tss.RPCTXStatus(rpcResponse.RPCResult.Status) + sendTxResponse.TransactionHash = rpcResponse.RPCResult.Hash + sendTxResponse.Code, err = r.parseErrorResultXDR(rpcResponse.RPCResult.ErrorResultXDR) + if err != nil { + return sendTxResponse, fmt.Errorf("parse error result xdr string: %w", err) + } + return sendTxResponse, nil +} + +func (r *rpcService) sendRPCRequest(method string, params map[string]string) (tss.RPCResponse, error) { + payload := map[string]interface{}{ + "jsonrpc": "2.0", + "id": 1, + "method": method, + "params": params, + } + jsonData, err := json.Marshal(payload) + + if err != nil { + return tss.RPCResponse{}, fmt.Errorf("marshaling payload") + } + + resp, err := r.HTTPClient.Post(r.RPCURL, "application/json", bytes.NewBuffer(jsonData)) + if err != nil { + return tss.RPCResponse{}, fmt.Errorf("%s: sending POST request to rpc: %v", method, err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return tss.RPCResponse{}, fmt.Errorf("%s: unmarshaling RPC response", method) + } + var res tss.RPCResponse + err = json.Unmarshal(body, &res) + if err != nil { + return tss.RPCResponse{}, fmt.Errorf("%s: parsing RPC response JSON", method) + } + if res.RPCResult == (tss.RPCResult{}) { + return tss.RPCResponse{}, fmt.Errorf("%s: response missing result field", method) + } + return res, nil +} + +func (r *rpcService) parseErrorResultXDR(errorResultXdr string) (tss.RPCTXCode, error) { + unMarshalErr := "unable to unmarshal errorResultXdr: %s" + decodedBytes, err := base64.StdEncoding.DecodeString(errorResultXdr) + if err != nil { + return tss.RPCTXCode{OtherCodes: tss.UnMarshalBinaryCode}, fmt.Errorf(unMarshalErr, errorResultXdr) + } + var errorResult xdr.TransactionResult + _, err = xdr3.Unmarshal(bytes.NewReader(decodedBytes), &errorResult) + if err != nil { + return tss.RPCTXCode{OtherCodes: tss.UnMarshalBinaryCode}, fmt.Errorf(unMarshalErr, errorResultXdr) + } + return tss.RPCTXCode{ + TxResultCode: errorResult.Result.Code, + }, nil +} +*/ diff --git a/internal/services/rpc_service_test_bu.go b/internal/services/rpc_service_test_bu.go new file mode 100644 index 0000000..00b83cb --- /dev/null +++ b/internal/services/rpc_service_test_bu.go @@ -0,0 +1,332 @@ +package services + +/* + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strings" + "testing" + + "github.com/stellar/go/xdr" + "github.com/stellar/wallet-backend/internal/tss" + "github.com/stellar/wallet-backend/internal/utils" + "github.com/stretchr/testify/assert" +) + +type errorReader struct{} + +func (e *errorReader) Read(p []byte) (n int, err error) { + return 0, fmt.Errorf("read error") +} + +func (e *errorReader) Close() error { + return nil +} + +func TestSendRPCRequest(t *testing.T) { + mockHTTPClient := utils.MockHTTPClient{} + rpcURL := "http://localhost:8000/soroban/rpc" + rpcService := NewRPCService(RPCServiceOptions{RPCURL: rpcURL, HTTPClient: &mockHTTPClient}) + method := "sendTransaction" + params := map[string]string{"transaction": "ABCD"} + payload := map[string]interface{}{ + "jsonrpc": "2.0", + "id": 1, + "method": method, + "params": params, + } + jsonData, _ := json.Marshal(payload) + t.Run("rpc_post_call_fails", func(t *testing.T) { + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(&http.Response{}, errors.New("RPC Connection fail")). + Once() + + resp, err := rpcService.sendRPCRequest(method, params) + + assert.Empty(t, resp) + assert.Equal(t, "sendTransaction: sending POST request to rpc: RPC Connection fail", err.Error()) + }) + + t.Run("unmarshaling_rpc_response_fails", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(&errorReader{}), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). + Once() + + resp, err := rpcService.sendRPCRequest(method, params) + + assert.Empty(t, resp) + assert.Equal(t, "sendTransaction: unmarshaling RPC response", err.Error()) + }) + + t.Run("unmarshaling_json_fails", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{invalid-json`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). + Once() + + resp, err := rpcService.sendRPCRequest(method, params) + + assert.Empty(t, resp) + assert.Equal(t, "sendTransaction: parsing RPC response JSON", err.Error()) + }) + + t.Run("response_has_no_result_field", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"status": "success"}`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). + Once() + + resp, err := rpcService.sendRPCRequest(method, params) + + assert.Empty(t, resp) + assert.Equal(t, "sendTransaction: response missing result field", err.Error()) + }) + + t.Run("response_has_status_field", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"result": {"status": "PENDING"}}`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). + Once() + + resp, err := rpcService.sendRPCRequest(method, params) + + assert.Equal(t, "PENDING", resp.Status) + assert.Empty(t, err) + }) + + t.Run("response_has_envelopexdr_field", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"result": {"envelopeXdr": "exdr"}}`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). + Once() + + resp, err := rpcService.sendRPCRequest(method, params) + + assert.Equal(t, "exdr", resp.EnvelopeXDR) + assert.Empty(t, err) + }) + + t.Run("response_has_resultxdr_field", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"result": {"resultXdr": "rxdr"}}`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). + Once() + + resp, err := rpcService.sendRPCRequest(method, params) + + assert.Equal(t, "rxdr", resp.ResultXDR) + assert.Empty(t, err) + }) + + t.Run("response_has_errorresultxdr_field", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"result": {"errorResultXdr": "exdr"}}`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). + Once() + + resp, err := rpcService.sendRPCRequest(method, params) + + assert.Equal(t, "exdr", resp.ErrorResultXDR) + assert.Empty(t, err) + }) + + t.Run("response_has_hash_field", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"result": {"hash": "hash"}}`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). + Once() + + resp, err := rpcService.sendRPCRequest(method, params) + + assert.Equal(t, "hash", resp.Hash) + assert.Empty(t, err) + }) + + t.Run("response_has_createdat_field", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"result": {"createdAt": "1234"}}`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). + Once() + + resp, err := rpcService.sendRPCRequest(method, params) + + assert.Equal(t, "1234", resp.CreatedAt) + assert.Empty(t, err) + }) +} + +func TestSendTransaction(t *testing.T) { + mockHTTPClient := utils.MockHTTPClient{} + rpcURL := "http://localhost:8000/soroban/rpc" + rpcService := NewRPCService(RPCServiceOptions{RPCURL: rpcURL, HTTPClient: &mockHTTPClient}) + method := "sendTransaction" + params := map[string]string{"transaction": "ABCD"} + payload := map[string]interface{}{ + "jsonrpc": "2.0", + "id": 1, + "method": method, + "params": params, + } + jsonData, _ := json.Marshal(payload) + + t.Run("rpc_request_fails", func(t *testing.T) { + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(&http.Response{}, errors.New("RPC Connection fail")). + Once() + + resp, err := rpcService.SendTransaction("ABCD") + + assert.Equal(t, tss.RPCFailCode, resp.Code.OtherCodes) + assert.Equal(t, "RPC fail: sendTransaction: sending POST request to rpc: RPC Connection fail", err.Error()) + + }) + t.Run("response_has_empty_errorResultXdr", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"result": {"status": "PENDING", "errorResultXdr": ""}}`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). + Once() + + resp, err := rpcService.SendTransaction("ABCD") + + assert.Equal(t, tss.PendingStatus, resp.Status) + assert.Equal(t, tss.UnMarshalBinaryCode, resp.Code.OtherCodes) + assert.Equal(t, "parse error result xdr string: unable to unmarshal errorResultXdr: ", err.Error()) + + }) + t.Run("response_has_unparsable_errorResultXdr", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"result": {"errorResultXdr": "ABC123"}}`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). + Once() + + resp, err := rpcService.SendTransaction("ABCD") + + assert.Equal(t, tss.UnMarshalBinaryCode, resp.Code.OtherCodes) + assert.Equal(t, "parse error result xdr string: unable to unmarshal errorResultXdr: ABC123", err.Error()) + }) + t.Run("response_has_errorResultXdr", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"result": {"errorResultXdr": "AAAAAAAAAMj////9AAAAAA=="}}`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). + Once() + + resp, err := rpcService.SendTransaction("ABCD") + + assert.Equal(t, xdr.TransactionResultCodeTxTooLate, resp.Code.TxResultCode) + assert.Empty(t, err) + }) +} +func TestGetTransaction(t *testing.T) { + mockHTTPClient := utils.MockHTTPClient{} + rpcURL := "http://localhost:8000/soroban/rpc" + rpcService := NewRPCService(RPCServiceOptions{RPCURL: rpcURL, HTTPClient: &mockHTTPClient}) + method := "getTransaction" + params := map[string]string{"hash": "XYZ"} + payload := map[string]interface{}{ + "jsonrpc": "2.0", + "id": 1, + "method": method, + "params": params, + } + jsonData, _ := json.Marshal(payload) + + t.Run("rpc_request_fails", func(t *testing.T) { + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(&http.Response{}, errors.New("RPC Connection fail")). + Once() + + resp, err := rpcService.GetTransaction("XYZ") + + assert.Equal(t, tss.ErrorStatus, resp.Status) + assert.Equal(t, "RPC Fail: getTransaction: sending POST request to rpc: RPC Connection fail", err.Error()) + + }) + t.Run("unable_to_parse_createdAt", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"result": {"status": "SUCCESS", "createdAt": "ABCD"}}`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). + Once() + + resp, err := rpcService.GetTransaction("XYZ") + + assert.Equal(t, tss.ErrorStatus, resp.Status) + assert.Equal(t, "unable to parse createAt: strconv.ParseInt: parsing \"ABCD\": invalid syntax", err.Error()) + }) + t.Run("response_has_createdAt_field", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"result": {"createdAt": "1234567"}}`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(httpResponse, nil). + Once() + + resp, err := rpcService.GetTransaction("XYZ") + + assert.Equal(t, int64(1234567), resp.CreatedAt) + assert.Empty(t, err) + }) +} +*/ diff --git a/internal/tss/channels/rpc_caller_service_channel.go b/internal/tss/channels/rpc_caller_service_channel.go index 33300c3..f2cceab 100644 --- a/internal/tss/channels/rpc_caller_service_channel.go +++ b/internal/tss/channels/rpc_caller_service_channel.go @@ -6,34 +6,35 @@ import ( "github.com/alitto/pond" "github.com/stellar/go/support/log" + "github.com/stellar/wallet-backend/internal/entities" "github.com/stellar/wallet-backend/internal/tss" "github.com/stellar/wallet-backend/internal/tss/router" "github.com/stellar/wallet-backend/internal/tss/services" "github.com/stellar/wallet-backend/internal/tss/store" - "github.com/stellar/wallet-backend/internal/tss/utils" ) type RPCCallerServiceChannelConfigs struct { - Store store.Store - TxService utils.TransactionService + TxManager services.TransactionManager Router router.Router + Store store.Store MaxBufferSize int MaxWorkers int } type rpcCallerServicePool struct { - Pool *pond.WorkerPool - TxService utils.TransactionService - ErrHandlerService services.Service - Store store.Store - Router router.Router + Pool *pond.WorkerPool + TxManager services.TransactionManager + Router router.Router + Store store.Store } +var ChannelName = "RPCCallerServiceChannel" + func NewRPCCallerServiceChannel(cfg RPCCallerServiceChannelConfigs) *rpcCallerServicePool { pool := pond.New(cfg.MaxBufferSize, cfg.MaxWorkers, pond.Strategy(pond.Balanced())) return &rpcCallerServicePool{ Pool: pool, - TxService: cfg.TxService, + TxManager: cfg.TxManager, Store: cfg.Store, Router: cfg.Router, } @@ -50,22 +51,24 @@ func (p *rpcCallerServicePool) Receive(payload tss.Payload) { ctx := context.Background() // Create a new transaction record in the transactions table. - err := p.Store.UpsertTransaction(ctx, payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.NewStatus) + err := p.Store.UpsertTransaction(ctx, payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.RPCTXStatus{OtherStatus: tss.NewStatus}) if err != nil { - log.Errorf("Unable to upsert transaction into transactions table: %s", err.Error()) + log.Errorf("RPCCallerChannel: Unable to upsert transaction into transactions table: %e", err) return } - - rpcSendResp, err := BuildAndSubmitTransaction(ctx, "RPCCallerServiceChannel", payload, p.Store, p.TxService) + rpcSendResp, err := p.TxManager.BuildAndSubmitTransaction(ctx, ChannelName, payload) if err != nil { - log.Errorf(": Unable to sign and submit transaction: %s", err.Error()) + log.Errorf("RPCCallerChannel: Unable to sign and submit transaction: %e", err) return } payload.RpcSubmitTxResponse = rpcSendResp - if rpcSendResp.Status == tss.TryAgainLaterStatus || rpcSendResp.Status == tss.ErrorStatus { - p.Router.Route(payload) + if rpcSendResp.Status.RPCStatus == entities.TryAgainLaterStatus || rpcSendResp.Status.RPCStatus == entities.ErrorStatus { + err = p.Router.Route(payload) + if err != nil { + log.Errorf("RPCCallerChannel: Unable to route payload: %e", err) + } } } diff --git a/internal/tss/channels/rpc_caller_service_channel_test.go b/internal/tss/channels/rpc_caller_service_channel_test.go index 797b56d..a2a7fa2 100644 --- a/internal/tss/channels/rpc_caller_service_channel_test.go +++ b/internal/tss/channels/rpc_caller_service_channel_test.go @@ -5,15 +5,13 @@ import ( "errors" "testing" - "github.com/stellar/go/xdr" "github.com/stellar/wallet-backend/internal/db" "github.com/stellar/wallet-backend/internal/db/dbtest" + "github.com/stellar/wallet-backend/internal/entities" "github.com/stellar/wallet-backend/internal/tss" "github.com/stellar/wallet-backend/internal/tss/router" + "github.com/stellar/wallet-backend/internal/tss/services" "github.com/stellar/wallet-backend/internal/tss/store" - "github.com/stellar/wallet-backend/internal/tss/utils" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) @@ -24,10 +22,12 @@ func TestSend(t *testing.T) { require.NoError(t, err) defer dbConnectionPool.Close() store := store.NewStore(dbConnectionPool) - txServiceMock := utils.TransactionServiceMock{} + txManagerMock := services.TransactionManagerMock{} + routerMock := router.MockRouter{} cfgs := RPCCallerServiceChannelConfigs{ Store: store, - TxService: &txServiceMock, + TxManager: &txManagerMock, + Router: &routerMock, MaxBufferSize: 10, MaxWorkers: 10, } @@ -36,38 +36,100 @@ func TestSend(t *testing.T) { payload.WebhookURL = "www.stellar.com" payload.TransactionHash = "hash" payload.TransactionXDR = "xdr" - networkPass := "passphrase" - feeBumpTx := utils.BuildTestFeeBumpTransaction() - feeBumpTxXDR, _ := feeBumpTx.Base64() - sendResp := tss.RPCSendTxResponse{} - sendResp.Code.OtherCodes = tss.RPCFailCode - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(feeBumpTx, nil). - Once(). - On("NetworkPassphrase"). - Return(networkPass). - Once(). - On("SendTransaction", feeBumpTxXDR). - Return(sendResp, errors.New("RPC Fail")). + rpcResp := tss.RPCSendTxResponse{ + Status: tss.RPCTXStatus{RPCStatus: entities.TryAgainLaterStatus}, + } + payload.RpcSubmitTxResponse = rpcResp + + txManagerMock. + On("BuildAndSubmitTransaction", context.Background(), ChannelName, payload). + Return(rpcResp, nil). + Once() + + routerMock. + On("Route", payload). + Return(nil). Once() channel.Send(payload) channel.Stop() - var status string - err = dbConnectionPool.GetContext(context.Background(), &status, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, status, string(tss.NewStatus)) + routerMock.AssertCalled(t, "Route", payload) +} - var tryStatus int - feeBumpTxHash, _ := feeBumpTx.HashHex(networkPass) - err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) +func TestReceivee(t *testing.T) { + dbt := dbtest.Open(t) + defer dbt.Close() + dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) require.NoError(t, err) - assert.Equal(t, int(tss.RPCFailCode), tryStatus) + defer dbConnectionPool.Close() + store := store.NewStore(dbConnectionPool) + txManagerMock := services.TransactionManagerMock{} + routerMock := router.MockRouter{} + cfgs := RPCCallerServiceChannelConfigs{ + Store: store, + TxManager: &txManagerMock, + Router: &routerMock, + MaxBufferSize: 10, + MaxWorkers: 10, + } + channel := NewRPCCallerServiceChannel(cfgs) + payload := tss.Payload{} + payload.WebhookURL = "www.stellar.com" + payload.TransactionHash = "hash" + payload.TransactionXDR = "xdr" + + t.Run("build_and_submit_tx_fail", func(t *testing.T) { + txManagerMock. + On("BuildAndSubmitTransaction", context.Background(), ChannelName, payload). + Return(tss.RPCSendTxResponse{}, errors.New("build tx failed")). + Once() + + channel.Receive(payload) + + routerMock.AssertNotCalled(t, "Route", payload) + }) + + t.Run("payload_not_routed", func(t *testing.T) { + rpcResp := tss.RPCSendTxResponse{ + Status: tss.RPCTXStatus{RPCStatus: entities.PendingStatus}, + } + payload.RpcSubmitTxResponse = rpcResp + + txManagerMock. + On("BuildAndSubmitTransaction", context.Background(), ChannelName, payload). + Return(rpcResp, nil). + Once() + + channel.Receive(payload) + + routerMock.AssertNotCalled(t, "Route", payload) + }) + t.Run("payload_routed", func(t *testing.T) { + rpcResp := tss.RPCSendTxResponse{ + Status: tss.RPCTXStatus{RPCStatus: entities.ErrorStatus}, + } + payload.RpcSubmitTxResponse = rpcResp + + txManagerMock. + On("BuildAndSubmitTransaction", context.Background(), ChannelName, payload). + Return(rpcResp, nil). + Once() + + routerMock. + On("Route", payload). + Return(nil). + Once() + + channel.Receive(payload) + + routerMock.AssertCalled(t, "Route", payload) + }) + } +/* func TestReceive(t *testing.T) { dbt := dbtest.Open(t) defer dbt.Close() @@ -76,13 +138,20 @@ func TestReceive(t *testing.T) { require.NoError(t, err) defer dbConnectionPool.Close() store := store.NewStore(dbConnectionPool) - txServiceMock := utils.TransactionServiceMock{} + txServiceMock := services.TransactionServiceMock{} defer txServiceMock.AssertExpectations(t) routerMock := router.MockRouter{} defer routerMock.AssertExpectations(t) + rpcServiceMock := services.RPCServiceMock{} + defer rpcServiceMock.AssertExpectations(t) + txManager := services.NewTransactionManager(services.TransactionManagerConfigs{ + TxService: &txServiceMock, + RPCService: &rpcServiceMock, + Store: store, + }) cfgs := RPCCallerServiceChannelConfigs{ Store: store, - TxService: &txServiceMock, + TxManager: txManager, Router: &routerMock, MaxBufferSize: 1, MaxWorkers: 1, @@ -205,3 +274,4 @@ func TestReceive(t *testing.T) { }) } +*/ diff --git a/internal/tss/channels/utils.go b/internal/tss/channels/utils.go deleted file mode 100644 index a5c59cc..0000000 --- a/internal/tss/channels/utils.go +++ /dev/null @@ -1,46 +0,0 @@ -package channels - -import ( - "fmt" - - "github.com/stellar/wallet-backend/internal/tss" - "github.com/stellar/wallet-backend/internal/tss/store" - "github.com/stellar/wallet-backend/internal/tss/utils" - "golang.org/x/net/context" -) - -func BuildAndSubmitTransaction(ctx context.Context, channelName string, payload tss.Payload, store store.Store, txService utils.TransactionService) (tss.RPCSendTxResponse, error) { - feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(ctx, payload.TransactionXDR) - if err != nil { - return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to sign/build transaction: %s", channelName, err.Error()) - } - feeBumpTxHash, err := feeBumpTx.HashHex(txService.NetworkPassphrase()) - if err != nil { - return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to hashhex fee bump transaction: %s", channelName, err.Error()) - } - - feeBumpTxXDR, err := feeBumpTx.Base64() - if err != nil { - return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to base64 fee bump transaction: %s", channelName, err.Error()) - } - - err = store.UpsertTry(ctx, payload.TransactionHash, feeBumpTxHash, feeBumpTxXDR, tss.RPCTXCode{OtherCodes: tss.NewCode}) - if err != nil { - return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to upsert try in tries table: %s", channelName, err.Error()) - } - rpcSendResp, rpcErr := txService.SendTransaction(feeBumpTxXDR) - - err = store.UpsertTry(ctx, payload.TransactionHash, feeBumpTxHash, feeBumpTxXDR, rpcSendResp.Code) - if err != nil { - return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to upsert try in tries table: %s", channelName, err.Error()) - } - if rpcErr != nil && rpcSendResp.Code.OtherCodes == tss.RPCFailCode || rpcSendResp.Code.OtherCodes == tss.UnMarshalBinaryCode { - return tss.RPCSendTxResponse{}, fmt.Errorf("%s: RPC fail: %s", channelName, rpcErr.Error()) - } - - err = store.UpsertTransaction(ctx, payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, rpcSendResp.Status) - if err != nil { - return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to do the final update of tx in the transactions table: %s", channelName, err.Error()) - } - return rpcSendResp, nil -} diff --git a/internal/tss/channels/utils_test.go b/internal/tss/channels/utils_test.go deleted file mode 100644 index c3dcb97..0000000 --- a/internal/tss/channels/utils_test.go +++ /dev/null @@ -1,143 +0,0 @@ -package channels - -import ( - "context" - "errors" - "testing" - - "github.com/stellar/go/xdr" - "github.com/stellar/wallet-backend/internal/db" - "github.com/stellar/wallet-backend/internal/db/dbtest" - "github.com/stellar/wallet-backend/internal/tss" - "github.com/stellar/wallet-backend/internal/tss/store" - "github.com/stellar/wallet-backend/internal/tss/utils" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestBuildAndSubmitTransaction(t *testing.T) { - dbt := dbtest.Open(t) - defer dbt.Close() - - dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) - require.NoError(t, err) - defer dbConnectionPool.Close() - store := store.NewStore(dbConnectionPool) - txServiceMock := utils.TransactionServiceMock{} - networkPass := "passphrase" - feeBumpTx := utils.BuildTestFeeBumpTransaction() - feeBumpTxXDR, _ := feeBumpTx.Base64() - feeBumpTxHash, _ := feeBumpTx.HashHex(networkPass) - payload := tss.Payload{} - payload.WebhookURL = "www.stellar.com" - payload.TransactionHash = "hash" - payload.TransactionXDR = "xdr" - - _ = store.UpsertTransaction(context.Background(), payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.NewStatus) - t.Run("fail_on_tx_build_and_sign", func(t *testing.T) { - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(nil, errors.New("signing failed")). - Once() - - _, err := BuildAndSubmitTransaction(context.Background(), "channel", payload, store, &txServiceMock) - - assert.Equal(t, "channel: Unable to sign/build transaction: signing failed", err.Error()) - - var status string - err = dbConnectionPool.GetContext(context.Background(), &status, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, string(tss.NewStatus), status) - }) - - t.Run("rpc_call_fail", func(t *testing.T) { - sendResp := tss.RPCSendTxResponse{} - sendResp.Code.OtherCodes = tss.RPCFailCode - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(feeBumpTx, nil). - Once(). - On("NetworkPassphrase"). - Return(networkPass). - Once(). - On("SendTransaction", feeBumpTxXDR). - Return(sendResp, errors.New("RPC Fail")). - Once() - - _, err := BuildAndSubmitTransaction(context.Background(), "channel", payload, store, &txServiceMock) - - assert.Equal(t, "channel: RPC fail: RPC Fail", err.Error()) - - var txStatus string - err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, txStatus, string(tss.NewStatus)) - - var tryStatus int - err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) - require.NoError(t, err) - assert.Equal(t, int(tss.RPCFailCode), tryStatus) - }) - - t.Run("rpc_resp_unmarshaling_error", func(t *testing.T) { - sendResp := tss.RPCSendTxResponse{} - sendResp.Code.OtherCodes = tss.UnMarshalBinaryCode - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(feeBumpTx, nil). - Once(). - On("NetworkPassphrase"). - Return(networkPass). - Once(). - On("SendTransaction", feeBumpTxXDR). - Return(sendResp, errors.New("unable to unmarshal")). - Once() - - _, err := BuildAndSubmitTransaction(context.Background(), "channel", payload, store, &txServiceMock) - - assert.Equal(t, "channel: RPC fail: unable to unmarshal", err.Error()) - - var txStatus string - err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, txStatus, string(tss.NewStatus)) - - var tryStatus int - err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) - require.NoError(t, err) - assert.Equal(t, int(tss.UnMarshalBinaryCode), tryStatus) - }) - t.Run("rpc_returns_response", func(t *testing.T) { - sendResp := tss.RPCSendTxResponse{} - sendResp.Status = tss.TryAgainLaterStatus - sendResp.TransactionHash = feeBumpTxHash - sendResp.TransactionXDR = feeBumpTxXDR - sendResp.Code.TxResultCode = xdr.TransactionResultCodeTxInsufficientFee - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(feeBumpTx, nil). - Once(). - On("NetworkPassphrase"). - Return(networkPass). - Once(). - On("SendTransaction", feeBumpTxXDR). - Return(sendResp, nil). - Once() - - resp, err := BuildAndSubmitTransaction(context.Background(), "channel", payload, store, &txServiceMock) - - assert.Equal(t, tss.TryAgainLaterStatus, resp.Status) - assert.Equal(t, xdr.TransactionResultCodeTxInsufficientFee, resp.Code.TxResultCode) - assert.Empty(t, err) - - var txStatus string - err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, string(tss.TryAgainLaterStatus), txStatus) - - var tryStatus int - err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) - require.NoError(t, err) - assert.Equal(t, int(xdr.TransactionResultCodeTxInsufficientFee), tryStatus) - }) -} diff --git a/internal/tss/mocks.go b/internal/tss/mocks.go new file mode 100644 index 0000000..beb0128 --- /dev/null +++ b/internal/tss/mocks.go @@ -0,0 +1,19 @@ +package tss + +import "github.com/stretchr/testify/mock" + +type MockChannel struct { + mock.Mock +} + +func (m *MockChannel) Send(payload Payload) { + m.Called(payload) +} + +func (m *MockChannel) Receive(payload Payload) { + m.Called(payload) +} + +func (m *MockChannel) Stop() { + m.Called() +} diff --git a/internal/tss/router/mocks.go b/internal/tss/router/mocks.go index 3f4406c..2f269b7 100644 --- a/internal/tss/router/mocks.go +++ b/internal/tss/router/mocks.go @@ -11,6 +11,7 @@ type MockRouter struct { var _ Router = (*MockRouter)(nil) -func (r *MockRouter) Route(payload tss.Payload) { - r.Called(payload) +func (r *MockRouter) Route(payload tss.Payload) error { + args := r.Called(payload) + return args.Error(0) } diff --git a/internal/tss/router/router.go b/internal/tss/router/router.go index e0d3945..569dffc 100644 --- a/internal/tss/router/router.go +++ b/internal/tss/router/router.go @@ -1,25 +1,30 @@ package router import ( + "fmt" "slices" "github.com/stellar/go/xdr" + "github.com/stellar/wallet-backend/internal/entities" "github.com/stellar/wallet-backend/internal/tss" - "github.com/stellar/wallet-backend/internal/tss/services" ) type Router interface { - Route(payload tss.Payload) + Route(payload tss.Payload) error } type RouterConfigs struct { - ErrorHandlerService services.Service - WebhookHandlerService services.Service + RPCCallerChannel tss.Channel + ErrorJitterChannel tss.Channel + ErrorNonJitterChannel tss.Channel + WebhookChannel tss.Channel } type router struct { - ErrorHandlerService services.Service - WebhookHandlerService services.Service + RPCCallerChannel tss.Channel + ErrorJitterChannel tss.Channel + ErrorNonJitterChannel tss.Channel + WebhookChannel tss.Channel } var _ Router = (*router)(nil) @@ -33,33 +38,52 @@ var FinalErrorCodes = []xdr.TransactionResultCode{ xdr.TransactionResultCodeTxMalformed, } -var RetryErrorCodes = []xdr.TransactionResultCode{ +var NonJitterErrorCodes = []xdr.TransactionResultCode{ + xdr.TransactionResultCodeTxTooEarly, xdr.TransactionResultCodeTxTooLate, + xdr.TransactionResultCodeTxBadSeq, +} + +var JitterErrorCodes = []xdr.TransactionResultCode{ xdr.TransactionResultCodeTxInsufficientFee, xdr.TransactionResultCodeTxInternalError, - xdr.TransactionResultCodeTxBadSeq, } func NewRouter(cfg RouterConfigs) Router { return &router{ - ErrorHandlerService: cfg.ErrorHandlerService, - WebhookHandlerService: cfg.WebhookHandlerService, + RPCCallerChannel: cfg.RPCCallerChannel, + ErrorJitterChannel: cfg.ErrorJitterChannel, + ErrorNonJitterChannel: cfg.ErrorNonJitterChannel, + WebhookChannel: cfg.WebhookChannel, } } -func (r *router) Route(payload tss.Payload) { - switch payload.RpcSubmitTxResponse.Status { - case tss.TryAgainLaterStatus: - r.ErrorHandlerService.ProcessPayload(payload) - case tss.ErrorStatus: - if payload.RpcSubmitTxResponse.Code.OtherCodes == tss.NoCode { - if slices.Contains(RetryErrorCodes, payload.RpcSubmitTxResponse.Code.TxResultCode) { - r.ErrorHandlerService.ProcessPayload(payload) - } else if slices.Contains(FinalErrorCodes, payload.RpcSubmitTxResponse.Code.TxResultCode) { - r.WebhookHandlerService.ProcessPayload(payload) +func (r *router) Route(payload tss.Payload) error { + var channel tss.Channel + if payload.RpcSubmitTxResponse.Status.Status() != "" { + switch payload.RpcSubmitTxResponse.Status.Status() { + case string(tss.NewStatus): + channel = r.RPCCallerChannel + case string(entities.TryAgainLaterStatus): + channel = r.ErrorJitterChannel + case string(entities.ErrorStatus): + if payload.RpcSubmitTxResponse.Code.OtherCodes == tss.NoCode { + if slices.Contains(JitterErrorCodes, payload.RpcSubmitTxResponse.Code.TxResultCode) { + channel = r.ErrorJitterChannel + } else if slices.Contains(NonJitterErrorCodes, payload.RpcSubmitTxResponse.Code.TxResultCode) { + channel = r.ErrorNonJitterChannel + } else if slices.Contains(FinalErrorCodes, payload.RpcSubmitTxResponse.Code.TxResultCode) { + channel = r.WebhookChannel + } } + default: + // Do nothing for PENDING / DUPLICATE statuses + return nil } - default: - return } + if channel == nil { + return fmt.Errorf("payload could not be routed - channel is nil") + } + channel.Send(payload) + return nil } diff --git a/internal/tss/router/router_test.go b/internal/tss/router/router_test.go index 8a5907a..05d6156 100644 --- a/internal/tss/router/router_test.go +++ b/internal/tss/router/router_test.go @@ -3,49 +3,128 @@ package router import ( "testing" - "github.com/stellar/go/xdr" + "github.com/stellar/wallet-backend/internal/entities" "github.com/stellar/wallet-backend/internal/tss" - "github.com/stellar/wallet-backend/internal/tss/services" + "github.com/stretchr/testify/assert" ) func TestRouter(t *testing.T) { - errorHandlerService := services.MockService{} - defer errorHandlerService.AssertExpectations(t) - webhookHandlerService := services.MockService{} - router := NewRouter(RouterConfigs{ErrorHandlerService: &errorHandlerService, WebhookHandlerService: &webhookHandlerService}) - t.Run("status_try_again_later", func(t *testing.T) { + rpcCallerChannel := tss.MockChannel{} + defer rpcCallerChannel.AssertExpectations(t) + errorJitterChannel := tss.MockChannel{} + defer errorJitterChannel.AssertExpectations(t) + errorNonJitterChannel := tss.MockChannel{} + defer errorNonJitterChannel.AssertExpectations(t) + webhookChannel := tss.MockChannel{} + defer webhookChannel.AssertExpectations(t) + + router := NewRouter(RouterConfigs{ + RPCCallerChannel: &rpcCallerChannel, + ErrorJitterChannel: &errorJitterChannel, + ErrorNonJitterChannel: &errorNonJitterChannel, + WebhookChannel: &webhookChannel, + }) + t.Run("status_new_routes_to_rpc_caller_channel", func(t *testing.T) { payload := tss.Payload{} - payload.RpcSubmitTxResponse.Status = tss.TryAgainLaterStatus + payload.RpcSubmitTxResponse.Status = tss.RPCTXStatus{OtherStatus: tss.NewStatus} - errorHandlerService. - On("ProcessPayload", payload). + rpcCallerChannel. + On("Send", payload). Return(). Once() - router.Route(payload) + _ = router.Route(payload) + + rpcCallerChannel.AssertCalled(t, "Send", payload) }) - t.Run("error_status_route_to_error_handler_service", func(t *testing.T) { + t.Run("status_try_again_later_routes_to_error_jitter_channel", func(t *testing.T) { payload := tss.Payload{} - payload.RpcSubmitTxResponse.Status = tss.ErrorStatus - payload.RpcSubmitTxResponse.Code.TxResultCode = xdr.TransactionResultCodeTxInsufficientFee + payload.RpcSubmitTxResponse.Status = tss.RPCTXStatus{RPCStatus: entities.TryAgainLaterStatus} - errorHandlerService. - On("ProcessPayload", payload). + errorJitterChannel. + On("Send", payload). Return(). Once() - router.Route(payload) + _ = router.Route(payload) + + errorJitterChannel.AssertCalled(t, "Send", payload) }) - t.Run("error_status_route_to_webhook_handler_service", func(t *testing.T) { + t.Run("status_error_routes_to_error_jitter_channel", func(t *testing.T) { + for _, code := range JitterErrorCodes { + payload := tss.Payload{ + RpcSubmitTxResponse: tss.RPCSendTxResponse{ + Status: tss.RPCTXStatus{ + RPCStatus: entities.ErrorStatus, + }, + Code: tss.RPCTXCode{ + TxResultCode: code, + }, + }, + } + payload.RpcSubmitTxResponse.Code.TxResultCode = code + errorJitterChannel. + On("Send", payload). + Return(). + Once() + + _ = router.Route(payload) + + errorJitterChannel.AssertCalled(t, "Send", payload) + } + }) + t.Run("status_error_routes_to_error_non_jitter_channel", func(t *testing.T) { + for _, code := range NonJitterErrorCodes { + payload := tss.Payload{ + RpcSubmitTxResponse: tss.RPCSendTxResponse{ + Status: tss.RPCTXStatus{ + RPCStatus: entities.ErrorStatus, + }, + Code: tss.RPCTXCode{ + TxResultCode: code, + }, + }, + } + payload.RpcSubmitTxResponse.Code.TxResultCode = code + errorNonJitterChannel. + On("Send", payload). + Return(). + Once() + + _ = router.Route(payload) + + errorNonJitterChannel.AssertCalled(t, "Send", payload) + } + }) + t.Run("status_error_routes_to_webhook_channel", func(t *testing.T) { + for _, code := range FinalErrorCodes { + payload := tss.Payload{ + RpcSubmitTxResponse: tss.RPCSendTxResponse{ + Status: tss.RPCTXStatus{ + RPCStatus: entities.ErrorStatus, + }, + Code: tss.RPCTXCode{ + TxResultCode: code, + }, + }, + } + payload.RpcSubmitTxResponse.Code.TxResultCode = code + webhookChannel. + On("Send", payload). + Return(). + Once() + + _ = router.Route(payload) + + webhookChannel.AssertCalled(t, "Send", payload) + } + }) + t.Run("nil_channel_does_not_route", func(t *testing.T) { payload := tss.Payload{} - payload.RpcSubmitTxResponse.Status = tss.ErrorStatus - payload.RpcSubmitTxResponse.Code.TxResultCode = xdr.TransactionResultCodeTxInsufficientBalance - webhookHandlerService. - On("ProcessPayload", payload). - Return(). - Once() + err := router.Route(payload) - router.Route(payload) + errorJitterChannel.AssertNotCalled(t, "Send", payload) + assert.Equal(t, "payload could not be routed - channel is nil", err.Error()) }) } diff --git a/internal/tss/services/error_handler_service.go b/internal/tss/services/error_handler_service.go deleted file mode 100644 index a9458b7..0000000 --- a/internal/tss/services/error_handler_service.go +++ /dev/null @@ -1,19 +0,0 @@ -package services - -import ( - "github.com/stellar/wallet-backend/internal/tss" -) - -type errorHandlerService struct { - channel tss.Channel -} - -func NewErrorHandlerService(channel tss.Channel) Service { - return &errorHandlerService{ - channel: channel, - } -} - -func (p *errorHandlerService) ProcessPayload(payload tss.Payload) { - // fill in later -} diff --git a/internal/tss/services/mocks.go b/internal/tss/services/mocks.go index fff8db8..f628c1d 100644 --- a/internal/tss/services/mocks.go +++ b/internal/tss/services/mocks.go @@ -1,16 +1,62 @@ package services import ( + "context" + "io" + "net/http" + + "github.com/stellar/go/txnbuild" "github.com/stellar/wallet-backend/internal/tss" + "github.com/stretchr/testify/mock" ) -type MockService struct { +type MockHTTPClient struct { + mock.Mock +} + +func (s *MockHTTPClient) Post(url, contentType string, body io.Reader) (resp *http.Response, err error) { + args := s.Called(url, contentType, body) + return args.Get(0).(*http.Response), args.Error(1) +} + +type TransactionServiceMock struct { + mock.Mock +} + +var _ TransactionService = (*TransactionServiceMock)(nil) + +func (t *TransactionServiceMock) NetworkPassphrase() string { + args := t.Called() + return args.String(0) +} + +func (t *TransactionServiceMock) SignAndBuildNewFeeBumpTransaction(ctx context.Context, origTxXdr string) (*txnbuild.FeeBumpTransaction, error) { + args := t.Called(ctx, origTxXdr) + if result := args.Get(0); result != nil { + return result.(*txnbuild.FeeBumpTransaction), args.Error(1) + } + return nil, args.Error(1) + +} + +func (t *TransactionServiceMock) SendTransaction(transactionXdr string) (tss.RPCSendTxResponse, error) { + args := t.Called(transactionXdr) + return args.Get(0).(tss.RPCSendTxResponse), args.Error(1) +} + +func (t *TransactionServiceMock) GetTransaction(transactionHash string) (tss.RPCGetIngestTxResponse, error) { + args := t.Called(transactionHash) + return args.Get(0).(tss.RPCGetIngestTxResponse), args.Error(1) +} + +type TransactionManagerMock struct { mock.Mock } -var _ Service = (*MockService)(nil) +var _ TransactionManager = (*TransactionManagerMock)(nil) -func (s *MockService) ProcessPayload(payload tss.Payload) { - s.Called(payload) +func (t *TransactionManagerMock) BuildAndSubmitTransaction(ctx context.Context, channelName string, payload tss.Payload) (tss.RPCSendTxResponse, error) { + args := t.Called(ctx, channelName, payload) + return args.Get(0).(tss.RPCSendTxResponse), args.Error(1) } diff --git a/internal/tss/services/rpc_caller_service.go b/internal/tss/services/rpc_caller_service.go deleted file mode 100644 index 7f539eb..0000000 --- a/internal/tss/services/rpc_caller_service.go +++ /dev/null @@ -1,21 +0,0 @@ -package services - -import ( - "github.com/stellar/wallet-backend/internal/tss" -) - -type rpcCallerService struct { - channel tss.Channel -} - -var _ Service = (*rpcCallerService)(nil) - -func NewRPCCallerService(channel tss.Channel) Service { - return &rpcCallerService{ - channel: channel, - } -} - -func (p *rpcCallerService) ProcessPayload(payload tss.Payload) { - p.channel.Send(payload) -} diff --git a/internal/tss/services/transaction_manager.go b/internal/tss/services/transaction_manager.go new file mode 100644 index 0000000..7825582 --- /dev/null +++ b/internal/tss/services/transaction_manager.go @@ -0,0 +1,76 @@ +package services + +import ( + "context" + "fmt" + + "github.com/stellar/wallet-backend/internal/services" + "github.com/stellar/wallet-backend/internal/tss" + "github.com/stellar/wallet-backend/internal/tss/store" +) + +type TransactionManager interface { + BuildAndSubmitTransaction(ctx context.Context, channelName string, payload tss.Payload) (tss.RPCSendTxResponse, error) +} + +type TransactionManagerConfigs struct { + TxService TransactionService + RPCService services.RPCService + Store store.Store +} + +type transactionManager struct { + TxService TransactionService + RPCService services.RPCService + Store store.Store +} + +func NewTransactionManager(cfg TransactionManagerConfigs) *transactionManager { + return &transactionManager{ + TxService: cfg.TxService, + RPCService: cfg.RPCService, + Store: cfg.Store, + } +} + +func (t *transactionManager) BuildAndSubmitTransaction(ctx context.Context, channelName string, payload tss.Payload) (tss.RPCSendTxResponse, error) { + feeBumpTx, err := t.TxService.SignAndBuildNewFeeBumpTransaction(ctx, payload.TransactionXDR) + if err != nil { + return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to sign/build transaction: %w", channelName, err) + } + feeBumpTxHash, err := feeBumpTx.HashHex(t.TxService.NetworkPassphrase()) + if err != nil { + return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to hashhex fee bump transaction: %w", channelName, err) + } + + feeBumpTxXDR, err := feeBumpTx.Base64() + if err != nil { + return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to base64 fee bump transaction: %w", channelName, err) + } + + err = t.Store.UpsertTry(ctx, payload.TransactionHash, feeBumpTxHash, feeBumpTxXDR, tss.RPCTXCode{OtherCodes: tss.NewCode}) + if err != nil { + return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to upsert try in tries table: %w", channelName, err) + } + rpcResp, rpcErr := t.RPCService.SendTransaction(feeBumpTxXDR) + rpcSendResp, parseErr := tss.ParseToRPCSendTxResponse(feeBumpTxHash, rpcResp, rpcErr) + + err = t.Store.UpsertTry(ctx, payload.TransactionHash, feeBumpTxHash, feeBumpTxXDR, rpcSendResp.Code) + if err != nil { + return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to upsert try in tries table: %s", channelName, err.Error()) + } + + if parseErr != nil { + return rpcSendResp, fmt.Errorf("%s: RPC fail: %w", channelName, parseErr) + } + + if rpcErr != nil && rpcSendResp.Code.OtherCodes == tss.RPCFailCode || rpcSendResp.Code.OtherCodes == tss.UnmarshalBinaryCode { + return tss.RPCSendTxResponse{}, fmt.Errorf("%s: RPC fail: %w", channelName, rpcErr) + } + + err = t.Store.UpsertTransaction(ctx, payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, rpcSendResp.Status) + if err != nil { + return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to do the final update of tx in the transactions table: %s", channelName, err.Error()) + } + return rpcSendResp, nil +} diff --git a/internal/tss/services/transaction_manager_test.go b/internal/tss/services/transaction_manager_test.go new file mode 100644 index 0000000..ff64eee --- /dev/null +++ b/internal/tss/services/transaction_manager_test.go @@ -0,0 +1,195 @@ +package services + +import ( + "context" + "errors" + "testing" + + "github.com/stellar/go/xdr" + "github.com/stellar/wallet-backend/internal/db" + "github.com/stellar/wallet-backend/internal/db/dbtest" + "github.com/stellar/wallet-backend/internal/entities" + "github.com/stellar/wallet-backend/internal/services" + "github.com/stellar/wallet-backend/internal/tss" + "github.com/stellar/wallet-backend/internal/tss/store" + "github.com/stellar/wallet-backend/internal/tss/utils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestBuildAndSubmitTransaction(t *testing.T) { + dbt := dbtest.Open(t) + defer dbt.Close() + + dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) + require.NoError(t, err) + defer dbConnectionPool.Close() + store := store.NewStore(dbConnectionPool) + txServiceMock := TransactionServiceMock{} + rpcServiceMock := services.RPCServiceMock{} + txManager := NewTransactionManager(TransactionManagerConfigs{ + TxService: &txServiceMock, + RPCService: &rpcServiceMock, + Store: store, + }) + networkPass := "passphrase" + feeBumpTx := utils.BuildTestFeeBumpTransaction() + feeBumpTxXDR, _ := feeBumpTx.Base64() + feeBumpTxHash, _ := feeBumpTx.HashHex(networkPass) + payload := tss.Payload{} + payload.WebhookURL = "www.stellar.com" + payload.TransactionHash = "hash" + payload.TransactionXDR = "xdr" + + t.Run("fail_on_tx_build_and_sign", func(t *testing.T) { + _ = store.UpsertTransaction(context.Background(), payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.RPCTXStatus{OtherStatus: tss.NewStatus}) + txServiceMock. + On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). + Return(nil, errors.New("signing failed")). + Once() + + _, err := txManager.BuildAndSubmitTransaction(context.Background(), "channel", payload) + + assert.Equal(t, "channel: Unable to sign/build transaction: signing failed", err.Error()) + + var status string + err = dbConnectionPool.GetContext(context.Background(), &status, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) + require.NoError(t, err) + assert.Equal(t, string(tss.NewStatus), status) + }) + + t.Run("rpc_call_fail", func(t *testing.T) { + _ = store.UpsertTransaction(context.Background(), payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.RPCTXStatus{OtherStatus: tss.NewStatus}) + sendResp := entities.RPCSendTransactionResult{Status: string(entities.ErrorStatus)} + + txServiceMock. + On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). + Return(feeBumpTx, nil). + Once(). + On("NetworkPassphrase"). + Return(networkPass). + Once() + rpcServiceMock. + On("SendTransaction", feeBumpTxXDR). + Return(sendResp, errors.New("RPC down")). + Once() + + _, err := txManager.BuildAndSubmitTransaction(context.Background(), "channel", payload) + + assert.Equal(t, "channel: RPC fail: RPC fail: RPC down", err.Error()) + + var txStatus string + err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) + require.NoError(t, err) + assert.Equal(t, txStatus, string(tss.NewStatus)) + + var tryStatus int + err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) + require.NoError(t, err) + assert.Equal(t, int(tss.RPCFailCode), tryStatus) + }) + + t.Run("rpc_resp_empty_errorresult_xdr", func(t *testing.T) { + _ = store.UpsertTransaction(context.Background(), payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.RPCTXStatus{OtherStatus: tss.NewStatus}) + sendResp := entities.RPCSendTransactionResult{ + Status: string(entities.PendingStatus), + ErrorResultXDR: "", + } + + txServiceMock. + On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). + Return(feeBumpTx, nil). + Once(). + On("NetworkPassphrase"). + Return(networkPass). + Once() + rpcServiceMock. + On("SendTransaction", feeBumpTxXDR). + Return(sendResp, nil). + Once() + + resp, err := txManager.BuildAndSubmitTransaction(context.Background(), "channel", payload) + + assert.Equal(t, entities.PendingStatus, resp.Status.RPCStatus) + assert.Equal(t, tss.EmptyCode, resp.Code.OtherCodes) + assert.Empty(t, err) + + var txStatus string + err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) + require.NoError(t, err) + assert.Equal(t, txStatus, string(entities.PendingStatus)) + + var tryStatus int + err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) + require.NoError(t, err) + assert.Equal(t, int(tss.EmptyCode), tryStatus) + }) + t.Run("rpc_resp_has_unparsable_errorresult_xdr", func(t *testing.T) { + _ = store.UpsertTransaction(context.Background(), payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.RPCTXStatus{OtherStatus: tss.NewStatus}) + sendResp := entities.RPCSendTransactionResult{ + Status: string(entities.ErrorStatus), + ErrorResultXDR: "ABCD", + } + + txServiceMock. + On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). + Return(feeBumpTx, nil). + Once(). + On("NetworkPassphrase"). + Return(networkPass). + Once() + rpcServiceMock. + On("SendTransaction", feeBumpTxXDR). + Return(sendResp, nil). + Once() + + _, err := txManager.BuildAndSubmitTransaction(context.Background(), "channel", payload) + + assert.Equal(t, "channel: RPC fail: parse error result xdr string: unable to unmarshal errorResultXDR: ABCD", err.Error()) + + var txStatus string + err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) + require.NoError(t, err) + assert.Equal(t, txStatus, string(tss.NewStatus)) + + var tryStatus int + err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) + require.NoError(t, err) + assert.Equal(t, int(tss.UnmarshalBinaryCode), tryStatus) + }) + t.Run("rpc_returns_response", func(t *testing.T) { + _ = store.UpsertTransaction(context.Background(), payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.RPCTXStatus{OtherStatus: tss.NewStatus}) + sendResp := entities.RPCSendTransactionResult{ + Status: string(entities.ErrorStatus), + ErrorResultXDR: "AAAAAAAAAMj////9AAAAAA==", + } + + txServiceMock. + On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). + Return(feeBumpTx, nil). + Once(). + On("NetworkPassphrase"). + Return(networkPass). + Once() + rpcServiceMock. + On("SendTransaction", feeBumpTxXDR). + Return(sendResp, nil). + Once() + + resp, err := txManager.BuildAndSubmitTransaction(context.Background(), "channel", payload) + + assert.Equal(t, entities.ErrorStatus, resp.Status.RPCStatus) + assert.Equal(t, xdr.TransactionResultCodeTxTooLate, resp.Code.TxResultCode) + assert.Empty(t, err) + + var txStatus string + err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) + require.NoError(t, err) + assert.Equal(t, string(entities.ErrorStatus), txStatus) + + var tryStatus int + err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) + require.NoError(t, err) + assert.Equal(t, int(xdr.TransactionResultCodeTxTooLate), tryStatus) + }) +} diff --git a/internal/tss/utils/transaction_service.go b/internal/tss/services/transaction_service.go similarity index 50% rename from internal/tss/utils/transaction_service.go rename to internal/tss/services/transaction_service.go index d6bbcdf..b65bae1 100644 --- a/internal/tss/utils/transaction_service.go +++ b/internal/tss/services/transaction_service.go @@ -1,42 +1,25 @@ -package utils +package services import ( - "bytes" "context" - "encoding/base64" - "encoding/json" "fmt" - "io" - "net/http" - "strconv" - xdr3 "github.com/stellar/go-xdr/xdr3" "github.com/stellar/go/clients/horizonclient" "github.com/stellar/go/txnbuild" - "github.com/stellar/go/xdr" "github.com/stellar/wallet-backend/internal/signing" - "github.com/stellar/wallet-backend/internal/tss" tsserror "github.com/stellar/wallet-backend/internal/tss/errors" ) -type HTTPClient interface { - Post(url string, t string, body io.Reader) (resp *http.Response, err error) -} - type TransactionService interface { NetworkPassphrase() string SignAndBuildNewFeeBumpTransaction(ctx context.Context, origTxXdr string) (*txnbuild.FeeBumpTransaction, error) - SendTransaction(transactionXdr string) (tss.RPCSendTxResponse, error) - GetTransaction(transactionHash string) (tss.RPCGetIngestTxResponse, error) } type transactionService struct { DistributionAccountSignatureClient signing.SignatureClient ChannelAccountSignatureClient signing.SignatureClient HorizonClient horizonclient.ClientInterface - RPCURL string BaseFee int64 - HTTPClient HTTPClient } var _ TransactionService = (*transactionService)(nil) @@ -45,9 +28,7 @@ type TransactionServiceOptions struct { DistributionAccountSignatureClient signing.SignatureClient ChannelAccountSignatureClient signing.SignatureClient HorizonClient horizonclient.ClientInterface - RPCURL string BaseFee int64 - HTTPClient HTTPClient } func (o *TransactionServiceOptions) ValidateOptions() error { @@ -63,18 +44,10 @@ func (o *TransactionServiceOptions) ValidateOptions() error { return fmt.Errorf("horizon client cannot be nil") } - if o.RPCURL == "" { - return fmt.Errorf("rpc url cannot be empty") - } - if o.BaseFee < int64(txnbuild.MinBaseFee) { return fmt.Errorf("base fee is lower than the minimum network fee") } - if o.HTTPClient == nil { - return fmt.Errorf("http client cannot be nil") - } - return nil } @@ -86,9 +59,7 @@ func NewTransactionService(opts TransactionServiceOptions) (*transactionService, DistributionAccountSignatureClient: opts.DistributionAccountSignatureClient, ChannelAccountSignatureClient: opts.ChannelAccountSignatureClient, HorizonClient: opts.HorizonClient, - RPCURL: opts.RPCURL, BaseFee: opts.BaseFee, - HTTPClient: opts.HTTPClient, }, nil } @@ -154,89 +125,3 @@ func (t *transactionService) SignAndBuildNewFeeBumpTransaction(ctx context.Conte } return feeBumpTx, nil } - -func (t *transactionService) parseErrorResultXDR(errorResultXdr string) (tss.RPCTXCode, error) { - unMarshalErr := "unable to unmarshal errorResultXdr: %s" - decodedBytes, err := base64.StdEncoding.DecodeString(errorResultXdr) - if err != nil { - return tss.RPCTXCode{OtherCodes: tss.UnMarshalBinaryCode}, fmt.Errorf(unMarshalErr, errorResultXdr) - } - var errorResult xdr.TransactionResult - _, err = xdr3.Unmarshal(bytes.NewReader(decodedBytes), &errorResult) - if err != nil { - return tss.RPCTXCode{OtherCodes: tss.UnMarshalBinaryCode}, fmt.Errorf(unMarshalErr, errorResultXdr) - } - return tss.RPCTXCode{ - TxResultCode: errorResult.Result.Code, - }, nil -} - -func (t *transactionService) sendRPCRequest(method string, params map[string]string) (tss.RPCResponse, error) { - payload := map[string]interface{}{ - "jsonrpc": "2.0", - "id": 1, - "method": method, - "params": params, - } - jsonData, err := json.Marshal(payload) - - if err != nil { - return tss.RPCResponse{}, fmt.Errorf("marshaling payload") - } - - resp, err := t.HTTPClient.Post(t.RPCURL, "application/json", bytes.NewBuffer(jsonData)) - if err != nil { - return tss.RPCResponse{}, fmt.Errorf("%s: sending POST request to rpc: %v", method, err) - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return tss.RPCResponse{}, fmt.Errorf("%s: unmarshaling RPC response", method) - } - var res tss.RPCResponse - err = json.Unmarshal(body, &res) - if err != nil { - return tss.RPCResponse{}, fmt.Errorf("%s: parsing RPC response JSON", method) - } - if res.RPCResult == (tss.RPCResult{}) { - return tss.RPCResponse{}, fmt.Errorf("%s: response missing result field", method) - } - return res, nil -} - -func (t *transactionService) SendTransaction(transactionXdr string) (tss.RPCSendTxResponse, error) { - rpcResponse, err := t.sendRPCRequest("sendTransaction", map[string]string{"transaction": transactionXdr}) - sendTxResponse := tss.RPCSendTxResponse{} - sendTxResponse.TransactionXDR = transactionXdr - if err != nil { - sendTxResponse.Code.OtherCodes = tss.RPCFailCode - return sendTxResponse, fmt.Errorf("RPC fail: %w", err) - } - sendTxResponse.Status = tss.RPCTXStatus(rpcResponse.RPCResult.Status) - sendTxResponse.TransactionHash = rpcResponse.RPCResult.Hash - sendTxResponse.Code, err = t.parseErrorResultXDR(rpcResponse.RPCResult.ErrorResultXDR) - if err != nil { - return sendTxResponse, fmt.Errorf("parse error result xdr string: %w", err) - } - return sendTxResponse, nil -} - -func (t *transactionService) GetTransaction(transactionHash string) (tss.RPCGetIngestTxResponse, error) { - rpcResponse, err := t.sendRPCRequest("getTransaction", map[string]string{"hash": transactionHash}) - if err != nil { - return tss.RPCGetIngestTxResponse{Status: tss.ErrorStatus}, fmt.Errorf("RPC Fail: %s", err.Error()) - } - getIngestTxResponse := tss.RPCGetIngestTxResponse{ - Status: tss.RPCTXStatus(rpcResponse.RPCResult.Status), - EnvelopeXDR: rpcResponse.RPCResult.EnvelopeXDR, - ResultXDR: rpcResponse.RPCResult.ResultXDR, - } - if getIngestTxResponse.Status != tss.NotFoundStatus { - getIngestTxResponse.CreatedAt, err = strconv.ParseInt(rpcResponse.RPCResult.CreatedAt, 10, 64) - if err != nil { - return tss.RPCGetIngestTxResponse{Status: tss.ErrorStatus}, fmt.Errorf("unable to parse createAt: %w", err) - } - } - return getIngestTxResponse, nil -} diff --git a/internal/tss/services/transaction_service_test.go b/internal/tss/services/transaction_service_test.go new file mode 100644 index 0000000..4dd137c --- /dev/null +++ b/internal/tss/services/transaction_service_test.go @@ -0,0 +1,236 @@ +package services + +import ( + "context" + "errors" + "testing" + + "github.com/stellar/go/clients/horizonclient" + "github.com/stellar/go/keypair" + "github.com/stellar/go/protocols/horizon" + "github.com/stellar/go/txnbuild" + "github.com/stellar/wallet-backend/internal/signing" + tsserror "github.com/stellar/wallet-backend/internal/tss/errors" + "github.com/stellar/wallet-backend/internal/tss/utils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestValidateOptions(t *testing.T) { + t.Run("return_error_when_distribution_signature_client_nil", func(t *testing.T) { + opts := TransactionServiceOptions{ + DistributionAccountSignatureClient: nil, + ChannelAccountSignatureClient: &signing.SignatureClientMock{}, + HorizonClient: &horizonclient.MockClient{}, + BaseFee: 114, + } + err := opts.ValidateOptions() + assert.Equal(t, "distribution account signature client cannot be nil", err.Error()) + + }) + + t.Run("return_error_when_channel_signature_client_nil", func(t *testing.T) { + opts := TransactionServiceOptions{ + DistributionAccountSignatureClient: &signing.SignatureClientMock{}, + ChannelAccountSignatureClient: nil, + HorizonClient: &horizonclient.MockClient{}, + BaseFee: 114, + } + err := opts.ValidateOptions() + assert.Equal(t, "channel account signature client cannot be nil", err.Error()) + }) + + t.Run("return_error_when_horizon_client_nil", func(t *testing.T) { + opts := TransactionServiceOptions{ + DistributionAccountSignatureClient: &signing.SignatureClientMock{}, + ChannelAccountSignatureClient: &signing.SignatureClientMock{}, + HorizonClient: nil, + BaseFee: 114, + } + err := opts.ValidateOptions() + assert.Equal(t, "horizon client cannot be nil", err.Error()) + }) + + t.Run("return_error_when_base_fee_too_low", func(t *testing.T) { + opts := TransactionServiceOptions{ + DistributionAccountSignatureClient: &signing.SignatureClientMock{}, + ChannelAccountSignatureClient: &signing.SignatureClientMock{}, + HorizonClient: &horizonclient.MockClient{}, + BaseFee: txnbuild.MinBaseFee - 10, + } + err := opts.ValidateOptions() + assert.Equal(t, "base fee is lower than the minimum network fee", err.Error()) + }) +} + +func TestSignAndBuildNewFeeBumpTransaction(t *testing.T) { + distributionAccountSignatureClient := signing.SignatureClientMock{} + defer distributionAccountSignatureClient.AssertExpectations(t) + channelAccountSignatureClient := signing.SignatureClientMock{} + defer channelAccountSignatureClient.AssertExpectations(t) + horizonClient := horizonclient.MockClient{} + defer horizonClient.AssertExpectations(t) + txService, _ := NewTransactionService(TransactionServiceOptions{ + DistributionAccountSignatureClient: &distributionAccountSignatureClient, + ChannelAccountSignatureClient: &channelAccountSignatureClient, + HorizonClient: &horizonClient, + BaseFee: 114, + }) + + txStr, _ := utils.BuildTestTransaction().Base64() + + t.Run("malformed_transaction_string", func(t *testing.T) { + feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), "abcd") + assert.Empty(t, feeBumpTx) + assert.ErrorIs(t, tsserror.OriginalXDRMalformed, err) + }) + + t.Run("channel_account_signature_client_get_account_public_key_err", func(t *testing.T) { + channelAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return("", errors.New("channel accounts unavailable")). + Once() + + feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) + assert.Empty(t, feeBumpTx) + assert.Equal(t, "getting channel account public key: channel accounts unavailable", err.Error()) + }) + + t.Run("horizon_client_get_account_detail_err", func(t *testing.T) { + channelAccount := keypair.MustRandom() + channelAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return(channelAccount.Address(), nil). + Once() + + horizonClient. + On("AccountDetail", horizonclient.AccountRequest{ + AccountID: channelAccount.Address(), + }). + Return(horizon.Account{}, errors.New("horizon down")). + Once() + + feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) + assert.Empty(t, feeBumpTx) + assert.Equal(t, "getting channel account details from horizon: horizon down", err.Error()) + }) + + t.Run("horizon_client_sign_stellar_transaction_w_channel_account_err", func(t *testing.T) { + channelAccount := keypair.MustRandom() + channelAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return(channelAccount.Address(), nil). + Once(). + On("SignStellarTransaction", context.Background(), mock.AnythingOfType("*txnbuild.Transaction"), []string{channelAccount.Address()}). + Return(nil, errors.New("unable to sign")). + Once() + + horizonClient. + On("AccountDetail", horizonclient.AccountRequest{ + AccountID: channelAccount.Address(), + }). + Return(horizon.Account{AccountID: channelAccount.Address(), Sequence: 1}, nil). + Once() + + feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) + assert.Empty(t, feeBumpTx) + assert.Equal(t, "signing transaction with channel account: unable to sign", err.Error()) + }) + + t.Run("distribution_account_signature_client_get_account_public_key_err", func(t *testing.T) { + channelAccount := keypair.MustRandom() + signedTx := txnbuild.Transaction{} + channelAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return(channelAccount.Address(), nil). + Once(). + On("SignStellarTransaction", context.Background(), mock.AnythingOfType("*txnbuild.Transaction"), []string{channelAccount.Address()}). + Return(&signedTx, nil). + Once() + + distributionAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return("", errors.New("client down")). + Once() + + horizonClient. + On("AccountDetail", horizonclient.AccountRequest{ + AccountID: channelAccount.Address(), + }). + Return(horizon.Account{AccountID: channelAccount.Address(), Sequence: 1}, nil). + Once() + + feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) + assert.Empty(t, feeBumpTx) + assert.Equal(t, "getting distribution account public key: client down", err.Error()) + }) + + t.Run("horizon_client_sign_stellar_transaction_w_distribition_account_err", func(t *testing.T) { + account := keypair.MustRandom() + signedTx := utils.BuildTestTransaction() + channelAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return(account.Address(), nil). + Once(). + On("SignStellarTransaction", context.Background(), mock.AnythingOfType("*txnbuild.Transaction"), []string{account.Address()}). + Return(signedTx, nil). + Once() + + distributionAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return(account.Address(), nil). + Once(). + On("SignStellarFeeBumpTransaction", context.Background(), mock.AnythingOfType("*txnbuild.FeeBumpTransaction")). + Return(nil, errors.New("unable to sign")). + Once() + + horizonClient. + On("AccountDetail", horizonclient.AccountRequest{ + AccountID: account.Address(), + }). + Return(horizon.Account{AccountID: account.Address(), Sequence: 1}, nil). + Once() + + feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) + assert.Empty(t, feeBumpTx) + assert.Equal(t, "signing the fee bump transaction with distribution account: unable to sign", err.Error()) + }) + + t.Run("returns_signed_tx", func(t *testing.T) { + account := keypair.MustRandom() + signedTx := utils.BuildTestTransaction() + testFeeBumpTx, _ := txnbuild.NewFeeBumpTransaction( + txnbuild.FeeBumpTransactionParams{ + Inner: signedTx, + FeeAccount: account.Address(), + BaseFee: int64(100), + }, + ) + channelAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return(account.Address(), nil). + Once(). + On("SignStellarTransaction", context.Background(), mock.AnythingOfType("*txnbuild.Transaction"), []string{account.Address()}). + Return(signedTx, nil). + Once() + + distributionAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return(account.Address(), nil). + Once(). + On("SignStellarFeeBumpTransaction", context.Background(), mock.AnythingOfType("*txnbuild.FeeBumpTransaction")). + Return(testFeeBumpTx, nil). + Once() + + horizonClient. + On("AccountDetail", horizonclient.AccountRequest{ + AccountID: account.Address(), + }). + Return(horizon.Account{AccountID: account.Address(), Sequence: 1}, nil). + Once() + + feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) + assert.Equal(t, feeBumpTx, testFeeBumpTx) + assert.Empty(t, err) + }) +} diff --git a/internal/tss/services/types.go b/internal/tss/services/types.go deleted file mode 100644 index f395190..0000000 --- a/internal/tss/services/types.go +++ /dev/null @@ -1,7 +0,0 @@ -package services - -import "github.com/stellar/wallet-backend/internal/tss" - -type Service interface { - ProcessPayload(payload tss.Payload) -} diff --git a/internal/tss/services/webhook_handler_service.go b/internal/tss/services/webhook_handler_service.go deleted file mode 100644 index 33c837e..0000000 --- a/internal/tss/services/webhook_handler_service.go +++ /dev/null @@ -1,19 +0,0 @@ -package services - -import ( - "github.com/stellar/wallet-backend/internal/tss" -) - -type webhookHandlerService struct { - channel tss.Channel -} - -func NewWebhookHandlerService(channel tss.Channel) Service { - return &webhookHandlerService{ - channel: channel, - } -} - -func (p *webhookHandlerService) ProcessPayload(payload tss.Payload) { - // fill in later -} diff --git a/internal/tss/store/store.go b/internal/tss/store/store.go index 866f1cc..b7953b0 100644 --- a/internal/tss/store/store.go +++ b/internal/tss/store/store.go @@ -38,7 +38,7 @@ func (s *store) UpsertTransaction(ctx context.Context, webhookURL string, txHash current_status = $4, updated_at = NOW(); ` - _, err := s.DB.ExecContext(ctx, q, txHash, txXDR, webhookURL, string(status)) + _, err := s.DB.ExecContext(ctx, q, txHash, txXDR, webhookURL, status.Status()) if err != nil { return fmt.Errorf("inserting/updatig tss transaction: %w", err) } diff --git a/internal/tss/store/store_test.go b/internal/tss/store/store_test.go index 57709ab..2987a27 100644 --- a/internal/tss/store/store_test.go +++ b/internal/tss/store/store_test.go @@ -7,6 +7,7 @@ import ( "github.com/stellar/go/xdr" "github.com/stellar/wallet-backend/internal/db" "github.com/stellar/wallet-backend/internal/db/dbtest" + "github.com/stellar/wallet-backend/internal/entities" "github.com/stellar/wallet-backend/internal/tss" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -20,7 +21,7 @@ func TestUpsertTransaction(t *testing.T) { defer dbConnectionPool.Close() store := NewStore(dbConnectionPool) t.Run("insert", func(t *testing.T) { - _ = store.UpsertTransaction(context.Background(), "www.stellar.org", "hash", "xdr", tss.NewStatus) + _ = store.UpsertTransaction(context.Background(), "www.stellar.org", "hash", "xdr", tss.RPCTXStatus{OtherStatus: tss.NewStatus}) var status string err = dbConnectionPool.GetContext(context.Background(), &status, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, "hash") @@ -29,13 +30,13 @@ func TestUpsertTransaction(t *testing.T) { }) t.Run("update", func(t *testing.T) { - _ = store.UpsertTransaction(context.Background(), "www.stellar.org", "hash", "xdr", tss.NewStatus) - _ = store.UpsertTransaction(context.Background(), "www.stellar.org", "hash", "xdr", tss.SuccessStatus) + _ = store.UpsertTransaction(context.Background(), "www.stellar.org", "hash", "xdr", tss.RPCTXStatus{OtherStatus: tss.NewStatus}) + _ = store.UpsertTransaction(context.Background(), "www.stellar.org", "hash", "xdr", tss.RPCTXStatus{RPCStatus: entities.SuccessStatus}) var status string err = dbConnectionPool.GetContext(context.Background(), &status, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, "hash") require.NoError(t, err) - assert.Equal(t, status, string(tss.SuccessStatus)) + assert.Equal(t, status, string(entities.SuccessStatus)) var numRows int err = dbConnectionPool.GetContext(context.Background(), &numRows, `SELECT count(*) FROM tss_transactions WHERE transaction_hash = $1`, "hash") diff --git a/internal/tss/types.go b/internal/tss/types.go index d3654ab..38c739e 100644 --- a/internal/tss/types.go +++ b/internal/tss/types.go @@ -1,19 +1,76 @@ package tss -import "github.com/stellar/go/xdr" +import ( + "bytes" + "encoding/base64" + "fmt" + "strconv" -type RPCTXStatus string + xdr3 "github.com/stellar/go-xdr/xdr3" + "github.com/stellar/go/xdr" + "github.com/stellar/wallet-backend/internal/entities" +) + +type RPCGetIngestTxResponse struct { + // A status that indicated whether this transaction failed or successly made it to the ledger + Status entities.RPCStatus + // The raw TransactionEnvelope XDR for this transaction + EnvelopeXDR string + // The raw TransactionResult XDR of the envelopeXdr + ResultXDR string + // The unix timestamp of when the transaction was included in the ledger + CreatedAt int64 +} + +func ParseToRPCGetIngestTxResponse(result entities.RPCGetTransactionResult, err error) (RPCGetIngestTxResponse, error) { + if err != nil { + return RPCGetIngestTxResponse{Status: entities.ErrorStatus}, err + } + + getIngestTxResponse := RPCGetIngestTxResponse{ + Status: entities.RPCStatus(result.Status), + EnvelopeXDR: result.EnvelopeXDR, + ResultXDR: result.ResultXDR, + } + if getIngestTxResponse.Status != entities.NotFoundStatus { + getIngestTxResponse.CreatedAt, err = strconv.ParseInt(result.CreatedAt, 10, 64) + if err != nil { + return RPCGetIngestTxResponse{Status: entities.ErrorStatus}, fmt.Errorf("unable to parse createdAt: %w", err) + } + } + return getIngestTxResponse, nil +} + +type OtherStatus string type OtherCodes int32 type TransactionResultCode int32 +const ( + NewStatus OtherStatus = "NEW" + NoStatus OtherStatus = "" +) + +type RPCTXStatus struct { + RPCStatus entities.RPCStatus + OtherStatus OtherStatus +} + +func (s RPCTXStatus) Status() string { + if s.OtherStatus != NoStatus { + return string(s.OtherStatus) + } + return string(s.RPCStatus) +} + const ( // Do not use NoCode NoCode OtherCodes = 0 // These values need to not overlap the values in xdr.TransactionResultCode NewCode OtherCodes = 100 RPCFailCode OtherCodes = 101 - UnMarshalBinaryCode OtherCodes = 102 + UnmarshalBinaryCode OtherCodes = 102 + EmptyCode OtherCodes = 103 ) type RPCTXCode struct { @@ -28,31 +85,6 @@ func (c RPCTXCode) Code() int { return int(c.TxResultCode) } -const ( - // Brand new transaction, not sent to RPC yet - NewStatus RPCTXStatus = "NEW" - // RPC sendTransaction statuses - PendingStatus RPCTXStatus = "PENDING" - DuplicateStatus RPCTXStatus = "DUPLICATE" - TryAgainLaterStatus RPCTXStatus = "TRY_AGAIN_LATER" - ErrorStatus RPCTXStatus = "ERROR" - // RPC getTransaction(s) statuses - NotFoundStatus RPCTXStatus = "NOT_FOUND" - FailedStatus RPCTXStatus = "FAILED" - SuccessStatus RPCTXStatus = "SUCCESS" -) - -type RPCGetIngestTxResponse struct { - // A status that indicated whether this transaction failed or successly made it to the ledger - Status RPCTXStatus - // The raw TransactionEnvelope XDR for this transaction - EnvelopeXDR string - // The raw TransactionResult XDR of the envelopeXdr - ResultXDR string - // The unix timestamp of when the transaction was included in the ledger - CreatedAt int64 -} - type RPCSendTxResponse struct { // The hash of the transaction submitted to RPC TransactionHash string @@ -64,6 +96,46 @@ type RPCSendTxResponse struct { Code RPCTXCode } +func ParseToRPCSendTxResponse(transactionXDR string, result entities.RPCSendTransactionResult, err error) (RPCSendTxResponse, error) { + sendTxResponse := RPCSendTxResponse{} + sendTxResponse.TransactionXDR = transactionXDR + if err != nil { + fmt.Println("RPC FAIL ON THIS?") + sendTxResponse.Status.RPCStatus = entities.ErrorStatus + sendTxResponse.Code.OtherCodes = RPCFailCode + return sendTxResponse, fmt.Errorf("RPC fail: %w", err) + } + sendTxResponse.Status.RPCStatus = entities.RPCStatus(result.Status) + sendTxResponse.TransactionHash = result.Hash + fmt.Println("abt to call parse") + sendTxResponse.Code, err = parseSendTransactionErrorXDR(result.ErrorResultXDR) + if err != nil { + return sendTxResponse, fmt.Errorf("parse error result xdr string: %w", err) + } + return sendTxResponse, nil +} + +func parseSendTransactionErrorXDR(errorResultXDR string) (RPCTXCode, error) { + if errorResultXDR == "" { + return RPCTXCode{ + OtherCodes: EmptyCode, + }, nil + } + unmarshalErr := "unable to unmarshal errorResultXDR: %s" + decodedBytes, err := base64.StdEncoding.DecodeString(errorResultXDR) + if err != nil { + return RPCTXCode{OtherCodes: UnmarshalBinaryCode}, fmt.Errorf(unmarshalErr, errorResultXDR) + } + var errorResult xdr.TransactionResult + _, err = xdr3.Unmarshal(bytes.NewReader(decodedBytes), &errorResult) + if err != nil { + return RPCTXCode{OtherCodes: UnmarshalBinaryCode}, fmt.Errorf(unmarshalErr, errorResultXDR) + } + return RPCTXCode{ + TxResultCode: errorResult.Result.Code, + }, nil +} + type Payload struct { WebhookURL string // The hash of the transaction xdr submitted by the client - the id of the transaction submitted by a client @@ -76,19 +148,6 @@ type Payload struct { RpcGetIngestTxResponse RPCGetIngestTxResponse } -type RPCResult struct { - Status string `json:"status"` - EnvelopeXDR string `json:"envelopeXdr"` - ResultXDR string `json:"resultXdr"` - ErrorResultXDR string `json:"errorResultXdr"` - Hash string `json:"hash"` - CreatedAt string `json:"createdAt"` -} - -type RPCResponse struct { - RPCResult `json:"result"` -} - type Channel interface { Send(payload Payload) Receive(payload Payload) diff --git a/internal/tss/types_bu.go b/internal/tss/types_bu.go new file mode 100644 index 0000000..78d3ce1 --- /dev/null +++ b/internal/tss/types_bu.go @@ -0,0 +1,98 @@ +package tss + +/* +import "github.com/stellar/go/xdr" + +type RPCTXStatus string +type OtherCodes int32 + +type TransactionResultCode int32 + +const ( + // Do not use NoCode + NoCode OtherCodes = 0 + // These values need to not overlap the values in xdr.TransactionResultCode + NewCode OtherCodes = 100 + RPCFailCode OtherCodes = 101 + UnMarshalBinaryCode OtherCodes = 102 +) + +type RPCTXCode struct { + TxResultCode xdr.TransactionResultCode + OtherCodes OtherCodes +} + +func (c RPCTXCode) Code() int { + if c.OtherCodes != NoCode { + return int(c.OtherCodes) + } + return int(c.TxResultCode) +} + +const ( + // Brand new transaction, not sent to RPC yet + NewStatus RPCTXStatus = "NEW" + // RPC sendTransaction statuses + PendingStatus RPCTXStatus = "PENDING" + DuplicateStatus RPCTXStatus = "DUPLICATE" + TryAgainLaterStatus RPCTXStatus = "TRY_AGAIN_LATER" + ErrorStatus RPCTXStatus = "ERROR" + // RPC getTransaction(s) statuses + NotFoundStatus RPCTXStatus = "NOT_FOUND" + FailedStatus RPCTXStatus = "FAILED" + SuccessStatus RPCTXStatus = "SUCCESS" +) + +type RPCGetIngestTxResponse struct { + // A status that indicated whether this transaction failed or successly made it to the ledger + Status RPCTXStatus + // The raw TransactionEnvelope XDR for this transaction + EnvelopeXDR string + // The raw TransactionResult XDR of the envelopeXdr + ResultXDR string + // The unix timestamp of when the transaction was included in the ledger + CreatedAt int64 +} + +type RPCSendTxResponse struct { + // The hash of the transaction submitted to RPC + TransactionHash string + TransactionXDR string + // The status of an RPC sendTransaction call. Can be one of [PENDING, DUPLICATE, TRY_AGAIN_LATER, ERROR] + Status RPCTXStatus + // The (optional) error code that is derived by deserialzing the errorResultXdr string in the sendTransaction response + // list of possible errror codes: https://developers.stellar.org/docs/data/horizon/api-reference/errors/result-codes/transactions + Code RPCTXCode +} + +type Payload struct { + WebhookURL string + // The hash of the transaction xdr submitted by the client - the id of the transaction submitted by a client + TransactionHash string + // The xdr of the transaction + TransactionXDR string + // Relevant fields in an RPC sendTransaction response + RpcSubmitTxResponse RPCSendTxResponse + // Relevant fields in the transaction list inside the RPC getTransactions response + RpcGetIngestTxResponse RPCGetIngestTxResponse +} + +type RPCResult struct { + Status string `json:"status"` + EnvelopeXDR string `json:"envelopeXdr"` + ResultXDR string `json:"resultXdr"` + ErrorResultXDR string `json:"errorResultXdr"` + Hash string `json:"hash"` + CreatedAt string `json:"createdAt"` +} + +type RPCResponse struct { + RPCResult `json:"result"` +} + +type Channel interface { + Send(payload Payload) + Receive(payload Payload) + Stop() +} +*/ diff --git a/internal/tss/utils/mocks.go b/internal/tss/utils/mocks.go deleted file mode 100644 index 91163b5..0000000 --- a/internal/tss/utils/mocks.go +++ /dev/null @@ -1,51 +0,0 @@ -package utils - -import ( - "context" - "io" - "net/http" - - "github.com/stellar/go/txnbuild" - "github.com/stellar/wallet-backend/internal/tss" - - "github.com/stretchr/testify/mock" -) - -type MockHTTPClient struct { - mock.Mock -} - -func (s *MockHTTPClient) Post(url, contentType string, body io.Reader) (resp *http.Response, err error) { - args := s.Called(url, contentType, body) - return args.Get(0).(*http.Response), args.Error(1) -} - -type TransactionServiceMock struct { - mock.Mock -} - -var _ TransactionService = (*TransactionServiceMock)(nil) - -func (t *TransactionServiceMock) NetworkPassphrase() string { - args := t.Called() - return args.String(0) -} - -func (t *TransactionServiceMock) SignAndBuildNewFeeBumpTransaction(ctx context.Context, origTxXdr string) (*txnbuild.FeeBumpTransaction, error) { - args := t.Called(ctx, origTxXdr) - if result := args.Get(0); result != nil { - return result.(*txnbuild.FeeBumpTransaction), args.Error(1) - } - return nil, args.Error(1) - -} - -func (t *TransactionServiceMock) SendTransaction(transactionXdr string) (tss.RPCSendTxResponse, error) { - args := t.Called(transactionXdr) - return args.Get(0).(tss.RPCSendTxResponse), args.Error(1) -} - -func (t *TransactionServiceMock) GetTransaction(transactionHash string) (tss.RPCGetIngestTxResponse, error) { - args := t.Called(transactionHash) - return args.Get(0).(tss.RPCGetIngestTxResponse), args.Error(1) -} diff --git a/internal/tss/utils/transaction_service_test.go b/internal/tss/utils/transaction_service_test.go deleted file mode 100644 index 95fc8f7..0000000 --- a/internal/tss/utils/transaction_service_test.go +++ /dev/null @@ -1,613 +0,0 @@ -package utils - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "strings" - "testing" - - "github.com/stellar/go/clients/horizonclient" - "github.com/stellar/go/keypair" - "github.com/stellar/go/protocols/horizon" - "github.com/stellar/go/txnbuild" - "github.com/stellar/go/xdr" - "github.com/stellar/wallet-backend/internal/signing" - "github.com/stellar/wallet-backend/internal/tss" - tsserror "github.com/stellar/wallet-backend/internal/tss/errors" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" -) - -func TestValidateOptions(t *testing.T) { - t.Run("return_error_when_distribution_signature_client_nil", func(t *testing.T) { - opts := TransactionServiceOptions{ - DistributionAccountSignatureClient: nil, - ChannelAccountSignatureClient: &signing.SignatureClientMock{}, - HorizonClient: &horizonclient.MockClient{}, - RPCURL: "http://localhost:8000/soroban/rpc", - BaseFee: 114, - HTTPClient: &MockHTTPClient{}, - } - err := opts.ValidateOptions() - assert.Equal(t, "distribution account signature client cannot be nil", err.Error()) - - }) - - t.Run("return_error_when_channel_signature_client_nil", func(t *testing.T) { - opts := TransactionServiceOptions{ - DistributionAccountSignatureClient: &signing.SignatureClientMock{}, - ChannelAccountSignatureClient: nil, - HorizonClient: &horizonclient.MockClient{}, - RPCURL: "http://localhost:8000/soroban/rpc", - BaseFee: 114, - HTTPClient: &MockHTTPClient{}, - } - err := opts.ValidateOptions() - assert.Equal(t, "channel account signature client cannot be nil", err.Error()) - }) - - t.Run("return_error_when_horizon_client_nil", func(t *testing.T) { - opts := TransactionServiceOptions{ - DistributionAccountSignatureClient: &signing.SignatureClientMock{}, - ChannelAccountSignatureClient: &signing.SignatureClientMock{}, - HorizonClient: nil, - RPCURL: "http://localhost:8000/soroban/rpc", - BaseFee: 114, - HTTPClient: &MockHTTPClient{}, - } - err := opts.ValidateOptions() - assert.Equal(t, "horizon client cannot be nil", err.Error()) - }) - - t.Run("return_error_when_rpc_url_empty", func(t *testing.T) { - opts := TransactionServiceOptions{ - DistributionAccountSignatureClient: &signing.SignatureClientMock{}, - ChannelAccountSignatureClient: &signing.SignatureClientMock{}, - HorizonClient: &horizonclient.MockClient{}, - RPCURL: "", - BaseFee: 114, - HTTPClient: &MockHTTPClient{}, - } - err := opts.ValidateOptions() - assert.Equal(t, "rpc url cannot be empty", err.Error()) - }) - - t.Run("return_error_when_base_fee_too_low", func(t *testing.T) { - opts := TransactionServiceOptions{ - DistributionAccountSignatureClient: &signing.SignatureClientMock{}, - ChannelAccountSignatureClient: &signing.SignatureClientMock{}, - HorizonClient: &horizonclient.MockClient{}, - RPCURL: "http://localhost:8000/soroban/rpc", - BaseFee: txnbuild.MinBaseFee - 10, - HTTPClient: &MockHTTPClient{}, - } - err := opts.ValidateOptions() - assert.Equal(t, "base fee is lower than the minimum network fee", err.Error()) - }) - - t.Run("return_error_http_client_nil", func(t *testing.T) { - opts := TransactionServiceOptions{ - DistributionAccountSignatureClient: &signing.SignatureClientMock{}, - ChannelAccountSignatureClient: &signing.SignatureClientMock{}, - HorizonClient: &horizonclient.MockClient{}, - RPCURL: "http://localhost:8000/soroban/rpc", - BaseFee: 114, - } - err := opts.ValidateOptions() - assert.Equal(t, "http client cannot be nil", err.Error()) - }) -} - -func TestSignAndBuildNewFeeBumpTransaction(t *testing.T) { - distributionAccountSignatureClient := signing.SignatureClientMock{} - defer distributionAccountSignatureClient.AssertExpectations(t) - channelAccountSignatureClient := signing.SignatureClientMock{} - defer channelAccountSignatureClient.AssertExpectations(t) - horizonClient := horizonclient.MockClient{} - defer horizonClient.AssertExpectations(t) - txService, _ := NewTransactionService(TransactionServiceOptions{ - DistributionAccountSignatureClient: &distributionAccountSignatureClient, - ChannelAccountSignatureClient: &channelAccountSignatureClient, - HorizonClient: &horizonClient, - RPCURL: "http://localhost:8000/soroban/rpc", - BaseFee: 114, - HTTPClient: &MockHTTPClient{}, - }) - - txStr, _ := BuildTestTransaction().Base64() - - t.Run("malformed_transaction_string", func(t *testing.T) { - feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), "abcd") - assert.Empty(t, feeBumpTx) - assert.ErrorIs(t, tsserror.OriginalXDRMalformed, err) - }) - - t.Run("channel_account_signature_client_get_account_public_key_err", func(t *testing.T) { - channelAccountSignatureClient. - On("GetAccountPublicKey", context.Background()). - Return("", errors.New("channel accounts unavailable")). - Once() - - feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) - assert.Empty(t, feeBumpTx) - assert.Equal(t, "getting channel account public key: channel accounts unavailable", err.Error()) - }) - - t.Run("horizon_client_get_account_detail_err", func(t *testing.T) { - channelAccount := keypair.MustRandom() - channelAccountSignatureClient. - On("GetAccountPublicKey", context.Background()). - Return(channelAccount.Address(), nil). - Once() - - horizonClient. - On("AccountDetail", horizonclient.AccountRequest{ - AccountID: channelAccount.Address(), - }). - Return(horizon.Account{}, errors.New("horizon down")). - Once() - - feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) - assert.Empty(t, feeBumpTx) - assert.Equal(t, "getting channel account details from horizon: horizon down", err.Error()) - }) - - t.Run("horizon_client_sign_stellar_transaction_w_channel_account_err", func(t *testing.T) { - channelAccount := keypair.MustRandom() - channelAccountSignatureClient. - On("GetAccountPublicKey", context.Background()). - Return(channelAccount.Address(), nil). - Once(). - On("SignStellarTransaction", context.Background(), mock.AnythingOfType("*txnbuild.Transaction"), []string{channelAccount.Address()}). - Return(nil, errors.New("unable to sign")). - Once() - - horizonClient. - On("AccountDetail", horizonclient.AccountRequest{ - AccountID: channelAccount.Address(), - }). - Return(horizon.Account{AccountID: channelAccount.Address(), Sequence: 1}, nil). - Once() - - feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) - assert.Empty(t, feeBumpTx) - assert.Equal(t, "signing transaction with channel account: unable to sign", err.Error()) - }) - - t.Run("distribution_account_signature_client_get_account_public_key_err", func(t *testing.T) { - channelAccount := keypair.MustRandom() - signedTx := txnbuild.Transaction{} - channelAccountSignatureClient. - On("GetAccountPublicKey", context.Background()). - Return(channelAccount.Address(), nil). - Once(). - On("SignStellarTransaction", context.Background(), mock.AnythingOfType("*txnbuild.Transaction"), []string{channelAccount.Address()}). - Return(&signedTx, nil). - Once() - - distributionAccountSignatureClient. - On("GetAccountPublicKey", context.Background()). - Return("", errors.New("client down")). - Once() - - horizonClient. - On("AccountDetail", horizonclient.AccountRequest{ - AccountID: channelAccount.Address(), - }). - Return(horizon.Account{AccountID: channelAccount.Address(), Sequence: 1}, nil). - Once() - - feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) - assert.Empty(t, feeBumpTx) - assert.Equal(t, "getting distribution account public key: client down", err.Error()) - }) - - t.Run("horizon_client_sign_stellar_transaction_w_distribition_account_err", func(t *testing.T) { - account := keypair.MustRandom() - signedTx := BuildTestTransaction() - channelAccountSignatureClient. - On("GetAccountPublicKey", context.Background()). - Return(account.Address(), nil). - Once(). - On("SignStellarTransaction", context.Background(), mock.AnythingOfType("*txnbuild.Transaction"), []string{account.Address()}). - Return(signedTx, nil). - Once() - - distributionAccountSignatureClient. - On("GetAccountPublicKey", context.Background()). - Return(account.Address(), nil). - Once(). - On("SignStellarFeeBumpTransaction", context.Background(), mock.AnythingOfType("*txnbuild.FeeBumpTransaction")). - Return(nil, errors.New("unable to sign")). - Once() - - horizonClient. - On("AccountDetail", horizonclient.AccountRequest{ - AccountID: account.Address(), - }). - Return(horizon.Account{AccountID: account.Address(), Sequence: 1}, nil). - Once() - - feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) - assert.Empty(t, feeBumpTx) - assert.Equal(t, "signing the fee bump transaction with distribution account: unable to sign", err.Error()) - }) - - t.Run("returns_signed_tx", func(t *testing.T) { - account := keypair.MustRandom() - signedTx := BuildTestTransaction() - testFeeBumpTx, _ := txnbuild.NewFeeBumpTransaction( - txnbuild.FeeBumpTransactionParams{ - Inner: signedTx, - FeeAccount: account.Address(), - BaseFee: int64(100), - }, - ) - channelAccountSignatureClient. - On("GetAccountPublicKey", context.Background()). - Return(account.Address(), nil). - Once(). - On("SignStellarTransaction", context.Background(), mock.AnythingOfType("*txnbuild.Transaction"), []string{account.Address()}). - Return(signedTx, nil). - Once() - - distributionAccountSignatureClient. - On("GetAccountPublicKey", context.Background()). - Return(account.Address(), nil). - Once(). - On("SignStellarFeeBumpTransaction", context.Background(), mock.AnythingOfType("*txnbuild.FeeBumpTransaction")). - Return(testFeeBumpTx, nil). - Once() - - horizonClient. - On("AccountDetail", horizonclient.AccountRequest{ - AccountID: account.Address(), - }). - Return(horizon.Account{AccountID: account.Address(), Sequence: 1}, nil). - Once() - - feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) - assert.Equal(t, feeBumpTx, testFeeBumpTx) - assert.Empty(t, err) - }) -} - -type errorReader struct{} - -func (e *errorReader) Read(p []byte) (n int, err error) { - return 0, fmt.Errorf("read error") -} - -func (e *errorReader) Close() error { - return nil -} - -func TestSendRPCRequest(t *testing.T) { - mockHTTPClient := MockHTTPClient{} - rpcURL := "http://localhost:8000/soroban/rpc" - txService, _ := NewTransactionService(TransactionServiceOptions{ - DistributionAccountSignatureClient: &signing.SignatureClientMock{}, - ChannelAccountSignatureClient: &signing.SignatureClientMock{}, - HorizonClient: &horizonclient.MockClient{}, - RPCURL: rpcURL, - BaseFee: 114, - HTTPClient: &mockHTTPClient, - }) - method := "sendTransaction" - params := map[string]string{"transaction": "ABCD"} - payload := map[string]interface{}{ - "jsonrpc": "2.0", - "id": 1, - "method": method, - "params": params, - } - jsonData, _ := json.Marshal(payload) - t.Run("rpc_post_call_fails", func(t *testing.T) { - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(&http.Response{}, errors.New("RPC Connection fail")). - Once() - - resp, err := txService.sendRPCRequest(method, params) - - assert.Empty(t, resp) - assert.Equal(t, "sendTransaction: sending POST request to rpc: RPC Connection fail", err.Error()) - }) - - t.Run("unmarshaling_rpc_response_fails", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(&errorReader{}), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.sendRPCRequest(method, params) - - assert.Empty(t, resp) - assert.Equal(t, "sendTransaction: unmarshaling RPC response", err.Error()) - }) - - t.Run("unmarshaling_json_fails", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{invalid-json`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.sendRPCRequest(method, params) - - assert.Empty(t, resp) - assert.Equal(t, "sendTransaction: parsing RPC response JSON", err.Error()) - }) - - t.Run("response_has_no_result_field", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"status": "success"}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.sendRPCRequest(method, params) - - assert.Empty(t, resp) - assert.Equal(t, "sendTransaction: response missing result field", err.Error()) - }) - - t.Run("response_has_status_field", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"status": "PENDING"}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.sendRPCRequest(method, params) - - assert.Equal(t, "PENDING", resp.Status) - assert.Empty(t, err) - }) - - t.Run("response_has_envelopexdr_field", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"envelopeXdr": "exdr"}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.sendRPCRequest(method, params) - - assert.Equal(t, "exdr", resp.EnvelopeXDR) - assert.Empty(t, err) - }) - - t.Run("response_has_resultxdr_field", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"resultXdr": "rxdr"}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.sendRPCRequest(method, params) - - assert.Equal(t, "rxdr", resp.ResultXDR) - assert.Empty(t, err) - }) - - t.Run("response_has_errorresultxdr_field", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"errorResultXdr": "exdr"}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.sendRPCRequest(method, params) - - assert.Equal(t, "exdr", resp.ErrorResultXDR) - assert.Empty(t, err) - }) - - t.Run("response_has_hash_field", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"hash": "hash"}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.sendRPCRequest(method, params) - - assert.Equal(t, "hash", resp.Hash) - assert.Empty(t, err) - }) - - t.Run("response_has_createdat_field", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"createdAt": "1234"}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.sendRPCRequest(method, params) - - assert.Equal(t, "1234", resp.CreatedAt) - assert.Empty(t, err) - }) -} - -func TestSendTransaction(t *testing.T) { - mockHTTPClient := MockHTTPClient{} - rpcURL := "http://localhost:8000/soroban/rpc" - txService, _ := NewTransactionService(TransactionServiceOptions{ - DistributionAccountSignatureClient: &signing.SignatureClientMock{}, - ChannelAccountSignatureClient: &signing.SignatureClientMock{}, - HorizonClient: &horizonclient.MockClient{}, - RPCURL: rpcURL, - BaseFee: 114, - HTTPClient: &mockHTTPClient, - }) - method := "sendTransaction" - params := map[string]string{"transaction": "ABCD"} - payload := map[string]interface{}{ - "jsonrpc": "2.0", - "id": 1, - "method": method, - "params": params, - } - jsonData, _ := json.Marshal(payload) - - t.Run("rpc_request_fails", func(t *testing.T) { - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(&http.Response{}, errors.New("RPC Connection fail")). - Once() - - resp, err := txService.SendTransaction("ABCD") - - assert.Equal(t, tss.RPCFailCode, resp.Code.OtherCodes) - assert.Equal(t, "RPC fail: sendTransaction: sending POST request to rpc: RPC Connection fail", err.Error()) - - }) - t.Run("response_has_empty_errorResultXdr", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"status": "PENDING", "errorResultXdr": ""}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.SendTransaction("ABCD") - - assert.Equal(t, tss.PendingStatus, resp.Status) - assert.Equal(t, tss.UnMarshalBinaryCode, resp.Code.OtherCodes) - assert.Equal(t, "parse error result xdr string: unable to unmarshal errorResultXdr: ", err.Error()) - - }) - t.Run("response_has_unparsable_errorResultXdr", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"errorResultXdr": "ABC123"}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.SendTransaction("ABCD") - - assert.Equal(t, tss.UnMarshalBinaryCode, resp.Code.OtherCodes) - assert.Equal(t, "parse error result xdr string: unable to unmarshal errorResultXdr: ABC123", err.Error()) - }) - t.Run("response_has_errorResultXdr", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"errorResultXdr": "AAAAAAAAAMj////9AAAAAA=="}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.SendTransaction("ABCD") - - assert.Equal(t, xdr.TransactionResultCodeTxTooLate, resp.Code.TxResultCode) - assert.Empty(t, err) - }) -} - -func TestGetTransaction(t *testing.T) { - mockHTTPClient := MockHTTPClient{} - rpcURL := "http://localhost:8000/soroban/rpc" - txService, _ := NewTransactionService(TransactionServiceOptions{ - DistributionAccountSignatureClient: &signing.SignatureClientMock{}, - ChannelAccountSignatureClient: &signing.SignatureClientMock{}, - HorizonClient: &horizonclient.MockClient{}, - RPCURL: rpcURL, - BaseFee: 114, - HTTPClient: &mockHTTPClient, - }) - method := "getTransaction" - params := map[string]string{"hash": "XYZ"} - payload := map[string]interface{}{ - "jsonrpc": "2.0", - "id": 1, - "method": method, - "params": params, - } - jsonData, _ := json.Marshal(payload) - - t.Run("rpc_request_fails", func(t *testing.T) { - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(&http.Response{}, errors.New("RPC Connection fail")). - Once() - - resp, err := txService.GetTransaction("XYZ") - - assert.Equal(t, tss.ErrorStatus, resp.Status) - assert.Equal(t, "RPC Fail: getTransaction: sending POST request to rpc: RPC Connection fail", err.Error()) - - }) - t.Run("unable_to_parse_createdAt", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"status": "SUCCESS", "createdAt": "ABCD"}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.GetTransaction("XYZ") - - assert.Equal(t, tss.ErrorStatus, resp.Status) - assert.Equal(t, "unable to parse createAt: strconv.ParseInt: parsing \"ABCD\": invalid syntax", err.Error()) - }) - t.Run("response_has_createdAt_field", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"createdAt": "1234567"}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.GetTransaction("XYZ") - - assert.Equal(t, int64(1234567), resp.CreatedAt) - assert.Empty(t, err) - }) - -} diff --git a/internal/utils/http_client.go b/internal/utils/http_client.go new file mode 100644 index 0000000..514abf0 --- /dev/null +++ b/internal/utils/http_client.go @@ -0,0 +1,21 @@ +package utils + +import ( + "io" + "net/http" + + "github.com/stretchr/testify/mock" +) + +type HTTPClient interface { + Post(url string, t string, body io.Reader) (resp *http.Response, err error) +} + +type MockHTTPClient struct { + mock.Mock +} + +func (s *MockHTTPClient) Post(url, contentType string, body io.Reader) (resp *http.Response, err error) { + args := s.Called(url, contentType, body) + return args.Get(0).(*http.Response), args.Error(1) +} From 0fb94e37a6a71ccb295ec6da4302d3f791d4ff24 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Wed, 25 Sep 2024 20:05:23 -0700 Subject: [PATCH 063/113] delete files --- internal/services/rpc_service_bu.go | 133 --------- internal/services/rpc_service_test_bu.go | 332 ----------------------- internal/tss/types_bu.go | 98 ------- 3 files changed, 563 deletions(-) delete mode 100644 internal/services/rpc_service_bu.go delete mode 100644 internal/services/rpc_service_test_bu.go delete mode 100644 internal/tss/types_bu.go diff --git a/internal/services/rpc_service_bu.go b/internal/services/rpc_service_bu.go deleted file mode 100644 index bfdcd2e..0000000 --- a/internal/services/rpc_service_bu.go +++ /dev/null @@ -1,133 +0,0 @@ -package services - -/* -package services - -import ( - "bytes" - "encoding/base64" - "encoding/json" - "fmt" - "io" - "net/http" - "strconv" - - xdr3 "github.com/stellar/go-xdr/xdr3" - "github.com/stellar/go/xdr" - "github.com/stellar/wallet-backend/internal/tss" -) - -type HTTPClient interface { - Post(url string, t string, body io.Reader) (resp *http.Response, err error) -} - -type RPCService interface { - SendTransaction(transactionXdr string) (tss.RPCSendTxResponse, error) - GetTransaction(transactionHash string) (tss.RPCGetIngestTxResponse, error) -} - -type RPCServiceOptions struct { - RPCURL string - HTTPClient HTTPClient -} - -type rpcService struct { - RPCURL string - HTTPClient HTTPClient -} - -var _ RPCService = (*rpcService)(nil) - -func NewRPCService(cfg RPCServiceOptions) *rpcService { - return &rpcService{ - RPCURL: cfg.RPCURL, - HTTPClient: cfg.HTTPClient, - } -} - -func (r *rpcService) GetTransaction(transactionHash string) (tss.RPCGetIngestTxResponse, error) { - rpcResponse, err := r.sendRPCRequest("getTransaction", map[string]string{"hash": transactionHash}) - if err != nil { - return tss.RPCGetIngestTxResponse{Status: tss.ErrorStatus}, fmt.Errorf("RPC Fail: %s", err.Error()) - } - getIngestTxResponse := tss.RPCGetIngestTxResponse{ - Status: tss.RPCTXStatus(rpcResponse.RPCResult.Status), - EnvelopeXDR: rpcResponse.RPCResult.EnvelopeXDR, - ResultXDR: rpcResponse.RPCResult.ResultXDR, - } - if getIngestTxResponse.Status != tss.NotFoundStatus { - getIngestTxResponse.CreatedAt, err = strconv.ParseInt(rpcResponse.RPCResult.CreatedAt, 10, 64) - if err != nil { - return tss.RPCGetIngestTxResponse{Status: tss.ErrorStatus}, fmt.Errorf("unable to parse createAt: %w", err) - } - } - return getIngestTxResponse, nil -} - -func (r *rpcService) SendTransaction(transactionXdr string) (tss.RPCSendTxResponse, error) { - rpcResponse, err := r.sendRPCRequest("sendTransaction", map[string]string{"transaction": transactionXdr}) - sendTxResponse := tss.RPCSendTxResponse{} - sendTxResponse.TransactionXDR = transactionXdr - if err != nil { - sendTxResponse.Code.OtherCodes = tss.RPCFailCode - return sendTxResponse, fmt.Errorf("RPC fail: %w", err) - } - sendTxResponse.Status = tss.RPCTXStatus(rpcResponse.RPCResult.Status) - sendTxResponse.TransactionHash = rpcResponse.RPCResult.Hash - sendTxResponse.Code, err = r.parseErrorResultXDR(rpcResponse.RPCResult.ErrorResultXDR) - if err != nil { - return sendTxResponse, fmt.Errorf("parse error result xdr string: %w", err) - } - return sendTxResponse, nil -} - -func (r *rpcService) sendRPCRequest(method string, params map[string]string) (tss.RPCResponse, error) { - payload := map[string]interface{}{ - "jsonrpc": "2.0", - "id": 1, - "method": method, - "params": params, - } - jsonData, err := json.Marshal(payload) - - if err != nil { - return tss.RPCResponse{}, fmt.Errorf("marshaling payload") - } - - resp, err := r.HTTPClient.Post(r.RPCURL, "application/json", bytes.NewBuffer(jsonData)) - if err != nil { - return tss.RPCResponse{}, fmt.Errorf("%s: sending POST request to rpc: %v", method, err) - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return tss.RPCResponse{}, fmt.Errorf("%s: unmarshaling RPC response", method) - } - var res tss.RPCResponse - err = json.Unmarshal(body, &res) - if err != nil { - return tss.RPCResponse{}, fmt.Errorf("%s: parsing RPC response JSON", method) - } - if res.RPCResult == (tss.RPCResult{}) { - return tss.RPCResponse{}, fmt.Errorf("%s: response missing result field", method) - } - return res, nil -} - -func (r *rpcService) parseErrorResultXDR(errorResultXdr string) (tss.RPCTXCode, error) { - unMarshalErr := "unable to unmarshal errorResultXdr: %s" - decodedBytes, err := base64.StdEncoding.DecodeString(errorResultXdr) - if err != nil { - return tss.RPCTXCode{OtherCodes: tss.UnMarshalBinaryCode}, fmt.Errorf(unMarshalErr, errorResultXdr) - } - var errorResult xdr.TransactionResult - _, err = xdr3.Unmarshal(bytes.NewReader(decodedBytes), &errorResult) - if err != nil { - return tss.RPCTXCode{OtherCodes: tss.UnMarshalBinaryCode}, fmt.Errorf(unMarshalErr, errorResultXdr) - } - return tss.RPCTXCode{ - TxResultCode: errorResult.Result.Code, - }, nil -} -*/ diff --git a/internal/services/rpc_service_test_bu.go b/internal/services/rpc_service_test_bu.go deleted file mode 100644 index 00b83cb..0000000 --- a/internal/services/rpc_service_test_bu.go +++ /dev/null @@ -1,332 +0,0 @@ -package services - -/* - -import ( - "bytes" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "strings" - "testing" - - "github.com/stellar/go/xdr" - "github.com/stellar/wallet-backend/internal/tss" - "github.com/stellar/wallet-backend/internal/utils" - "github.com/stretchr/testify/assert" -) - -type errorReader struct{} - -func (e *errorReader) Read(p []byte) (n int, err error) { - return 0, fmt.Errorf("read error") -} - -func (e *errorReader) Close() error { - return nil -} - -func TestSendRPCRequest(t *testing.T) { - mockHTTPClient := utils.MockHTTPClient{} - rpcURL := "http://localhost:8000/soroban/rpc" - rpcService := NewRPCService(RPCServiceOptions{RPCURL: rpcURL, HTTPClient: &mockHTTPClient}) - method := "sendTransaction" - params := map[string]string{"transaction": "ABCD"} - payload := map[string]interface{}{ - "jsonrpc": "2.0", - "id": 1, - "method": method, - "params": params, - } - jsonData, _ := json.Marshal(payload) - t.Run("rpc_post_call_fails", func(t *testing.T) { - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(&http.Response{}, errors.New("RPC Connection fail")). - Once() - - resp, err := rpcService.sendRPCRequest(method, params) - - assert.Empty(t, resp) - assert.Equal(t, "sendTransaction: sending POST request to rpc: RPC Connection fail", err.Error()) - }) - - t.Run("unmarshaling_rpc_response_fails", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(&errorReader{}), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := rpcService.sendRPCRequest(method, params) - - assert.Empty(t, resp) - assert.Equal(t, "sendTransaction: unmarshaling RPC response", err.Error()) - }) - - t.Run("unmarshaling_json_fails", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{invalid-json`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := rpcService.sendRPCRequest(method, params) - - assert.Empty(t, resp) - assert.Equal(t, "sendTransaction: parsing RPC response JSON", err.Error()) - }) - - t.Run("response_has_no_result_field", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"status": "success"}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := rpcService.sendRPCRequest(method, params) - - assert.Empty(t, resp) - assert.Equal(t, "sendTransaction: response missing result field", err.Error()) - }) - - t.Run("response_has_status_field", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"status": "PENDING"}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := rpcService.sendRPCRequest(method, params) - - assert.Equal(t, "PENDING", resp.Status) - assert.Empty(t, err) - }) - - t.Run("response_has_envelopexdr_field", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"envelopeXdr": "exdr"}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := rpcService.sendRPCRequest(method, params) - - assert.Equal(t, "exdr", resp.EnvelopeXDR) - assert.Empty(t, err) - }) - - t.Run("response_has_resultxdr_field", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"resultXdr": "rxdr"}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := rpcService.sendRPCRequest(method, params) - - assert.Equal(t, "rxdr", resp.ResultXDR) - assert.Empty(t, err) - }) - - t.Run("response_has_errorresultxdr_field", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"errorResultXdr": "exdr"}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := rpcService.sendRPCRequest(method, params) - - assert.Equal(t, "exdr", resp.ErrorResultXDR) - assert.Empty(t, err) - }) - - t.Run("response_has_hash_field", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"hash": "hash"}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := rpcService.sendRPCRequest(method, params) - - assert.Equal(t, "hash", resp.Hash) - assert.Empty(t, err) - }) - - t.Run("response_has_createdat_field", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"createdAt": "1234"}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := rpcService.sendRPCRequest(method, params) - - assert.Equal(t, "1234", resp.CreatedAt) - assert.Empty(t, err) - }) -} - -func TestSendTransaction(t *testing.T) { - mockHTTPClient := utils.MockHTTPClient{} - rpcURL := "http://localhost:8000/soroban/rpc" - rpcService := NewRPCService(RPCServiceOptions{RPCURL: rpcURL, HTTPClient: &mockHTTPClient}) - method := "sendTransaction" - params := map[string]string{"transaction": "ABCD"} - payload := map[string]interface{}{ - "jsonrpc": "2.0", - "id": 1, - "method": method, - "params": params, - } - jsonData, _ := json.Marshal(payload) - - t.Run("rpc_request_fails", func(t *testing.T) { - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(&http.Response{}, errors.New("RPC Connection fail")). - Once() - - resp, err := rpcService.SendTransaction("ABCD") - - assert.Equal(t, tss.RPCFailCode, resp.Code.OtherCodes) - assert.Equal(t, "RPC fail: sendTransaction: sending POST request to rpc: RPC Connection fail", err.Error()) - - }) - t.Run("response_has_empty_errorResultXdr", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"status": "PENDING", "errorResultXdr": ""}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := rpcService.SendTransaction("ABCD") - - assert.Equal(t, tss.PendingStatus, resp.Status) - assert.Equal(t, tss.UnMarshalBinaryCode, resp.Code.OtherCodes) - assert.Equal(t, "parse error result xdr string: unable to unmarshal errorResultXdr: ", err.Error()) - - }) - t.Run("response_has_unparsable_errorResultXdr", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"errorResultXdr": "ABC123"}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := rpcService.SendTransaction("ABCD") - - assert.Equal(t, tss.UnMarshalBinaryCode, resp.Code.OtherCodes) - assert.Equal(t, "parse error result xdr string: unable to unmarshal errorResultXdr: ABC123", err.Error()) - }) - t.Run("response_has_errorResultXdr", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"errorResultXdr": "AAAAAAAAAMj////9AAAAAA=="}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := rpcService.SendTransaction("ABCD") - - assert.Equal(t, xdr.TransactionResultCodeTxTooLate, resp.Code.TxResultCode) - assert.Empty(t, err) - }) -} -func TestGetTransaction(t *testing.T) { - mockHTTPClient := utils.MockHTTPClient{} - rpcURL := "http://localhost:8000/soroban/rpc" - rpcService := NewRPCService(RPCServiceOptions{RPCURL: rpcURL, HTTPClient: &mockHTTPClient}) - method := "getTransaction" - params := map[string]string{"hash": "XYZ"} - payload := map[string]interface{}{ - "jsonrpc": "2.0", - "id": 1, - "method": method, - "params": params, - } - jsonData, _ := json.Marshal(payload) - - t.Run("rpc_request_fails", func(t *testing.T) { - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(&http.Response{}, errors.New("RPC Connection fail")). - Once() - - resp, err := rpcService.GetTransaction("XYZ") - - assert.Equal(t, tss.ErrorStatus, resp.Status) - assert.Equal(t, "RPC Fail: getTransaction: sending POST request to rpc: RPC Connection fail", err.Error()) - - }) - t.Run("unable_to_parse_createdAt", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"status": "SUCCESS", "createdAt": "ABCD"}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := rpcService.GetTransaction("XYZ") - - assert.Equal(t, tss.ErrorStatus, resp.Status) - assert.Equal(t, "unable to parse createAt: strconv.ParseInt: parsing \"ABCD\": invalid syntax", err.Error()) - }) - t.Run("response_has_createdAt_field", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"createdAt": "1234567"}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := rpcService.GetTransaction("XYZ") - - assert.Equal(t, int64(1234567), resp.CreatedAt) - assert.Empty(t, err) - }) -} -*/ diff --git a/internal/tss/types_bu.go b/internal/tss/types_bu.go deleted file mode 100644 index 78d3ce1..0000000 --- a/internal/tss/types_bu.go +++ /dev/null @@ -1,98 +0,0 @@ -package tss - -/* -import "github.com/stellar/go/xdr" - -type RPCTXStatus string -type OtherCodes int32 - -type TransactionResultCode int32 - -const ( - // Do not use NoCode - NoCode OtherCodes = 0 - // These values need to not overlap the values in xdr.TransactionResultCode - NewCode OtherCodes = 100 - RPCFailCode OtherCodes = 101 - UnMarshalBinaryCode OtherCodes = 102 -) - -type RPCTXCode struct { - TxResultCode xdr.TransactionResultCode - OtherCodes OtherCodes -} - -func (c RPCTXCode) Code() int { - if c.OtherCodes != NoCode { - return int(c.OtherCodes) - } - return int(c.TxResultCode) -} - -const ( - // Brand new transaction, not sent to RPC yet - NewStatus RPCTXStatus = "NEW" - // RPC sendTransaction statuses - PendingStatus RPCTXStatus = "PENDING" - DuplicateStatus RPCTXStatus = "DUPLICATE" - TryAgainLaterStatus RPCTXStatus = "TRY_AGAIN_LATER" - ErrorStatus RPCTXStatus = "ERROR" - // RPC getTransaction(s) statuses - NotFoundStatus RPCTXStatus = "NOT_FOUND" - FailedStatus RPCTXStatus = "FAILED" - SuccessStatus RPCTXStatus = "SUCCESS" -) - -type RPCGetIngestTxResponse struct { - // A status that indicated whether this transaction failed or successly made it to the ledger - Status RPCTXStatus - // The raw TransactionEnvelope XDR for this transaction - EnvelopeXDR string - // The raw TransactionResult XDR of the envelopeXdr - ResultXDR string - // The unix timestamp of when the transaction was included in the ledger - CreatedAt int64 -} - -type RPCSendTxResponse struct { - // The hash of the transaction submitted to RPC - TransactionHash string - TransactionXDR string - // The status of an RPC sendTransaction call. Can be one of [PENDING, DUPLICATE, TRY_AGAIN_LATER, ERROR] - Status RPCTXStatus - // The (optional) error code that is derived by deserialzing the errorResultXdr string in the sendTransaction response - // list of possible errror codes: https://developers.stellar.org/docs/data/horizon/api-reference/errors/result-codes/transactions - Code RPCTXCode -} - -type Payload struct { - WebhookURL string - // The hash of the transaction xdr submitted by the client - the id of the transaction submitted by a client - TransactionHash string - // The xdr of the transaction - TransactionXDR string - // Relevant fields in an RPC sendTransaction response - RpcSubmitTxResponse RPCSendTxResponse - // Relevant fields in the transaction list inside the RPC getTransactions response - RpcGetIngestTxResponse RPCGetIngestTxResponse -} - -type RPCResult struct { - Status string `json:"status"` - EnvelopeXDR string `json:"envelopeXdr"` - ResultXDR string `json:"resultXdr"` - ErrorResultXDR string `json:"errorResultXdr"` - Hash string `json:"hash"` - CreatedAt string `json:"createdAt"` -} - -type RPCResponse struct { - RPCResult `json:"result"` -} - -type Channel interface { - Send(payload Payload) - Receive(payload Payload) - Stop() -} -*/ From df7ae05d4d8772179152f613ac6718006663063a Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Wed, 25 Sep 2024 20:22:45 -0700 Subject: [PATCH 064/113] remove unused code --- internal/tss/services/mocks.go | 11 ----------- internal/tss/types.go | 1 + 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/internal/tss/services/mocks.go b/internal/tss/services/mocks.go index f628c1d..3edcb26 100644 --- a/internal/tss/services/mocks.go +++ b/internal/tss/services/mocks.go @@ -2,8 +2,6 @@ package services import ( "context" - "io" - "net/http" "github.com/stellar/go/txnbuild" "github.com/stellar/wallet-backend/internal/tss" @@ -11,15 +9,6 @@ import ( "github.com/stretchr/testify/mock" ) -type MockHTTPClient struct { - mock.Mock -} - -func (s *MockHTTPClient) Post(url, contentType string, body io.Reader) (resp *http.Response, err error) { - args := s.Called(url, contentType, body) - return args.Get(0).(*http.Response), args.Error(1) -} - type TransactionServiceMock struct { mock.Mock } diff --git a/internal/tss/types.go b/internal/tss/types.go index 38c739e..6d2e828 100644 --- a/internal/tss/types.go +++ b/internal/tss/types.go @@ -22,6 +22,7 @@ type RPCGetIngestTxResponse struct { CreatedAt int64 } +//nolint:unused func ParseToRPCGetIngestTxResponse(result entities.RPCGetTransactionResult, err error) (RPCGetIngestTxResponse, error) { if err != nil { return RPCGetIngestTxResponse{Status: entities.ErrorStatus}, err From cf2dc18636bce1465ec7811f933fca18b214d0e6 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Wed, 25 Sep 2024 20:43:17 -0700 Subject: [PATCH 065/113] name change --- internal/serve/serve.go | 2 +- .../{rpc_caller_service_channel.go => rpc_caller_channel.go} | 2 +- ...ler_service_channel_test.go => rpc_caller_channel_test.go} | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) rename internal/tss/channels/{rpc_caller_service_channel.go => rpc_caller_channel.go} (95%) rename internal/tss/channels/{rpc_caller_service_channel_test.go => rpc_caller_channel_test.go} (98%) diff --git a/internal/serve/serve.go b/internal/serve/serve.go index 6ef3670..23c46d9 100644 --- a/internal/serve/serve.go +++ b/internal/serve/serve.go @@ -197,7 +197,7 @@ func initHandlerDeps(cfg Configs) (handlerDeps, error) { MaxBufferSize: cfg.RPCCallerServiceChannelBufferSize, MaxWorkers: cfg.RPCCallerServiceChannelMaxWorkers, } - rpcCallerServiceChannel := tsschannel.NewRPCCallerServiceChannel(tssChannelConfigs) + rpcCallerServiceChannel := tsschannel.NewRPCCallerChannel(tssChannelConfigs) router := tssrouter.NewRouter(tssrouter.RouterConfigs{ RPCCallerChannel: rpcCallerServiceChannel, diff --git a/internal/tss/channels/rpc_caller_service_channel.go b/internal/tss/channels/rpc_caller_channel.go similarity index 95% rename from internal/tss/channels/rpc_caller_service_channel.go rename to internal/tss/channels/rpc_caller_channel.go index f2cceab..5d8e036 100644 --- a/internal/tss/channels/rpc_caller_service_channel.go +++ b/internal/tss/channels/rpc_caller_channel.go @@ -30,7 +30,7 @@ type rpcCallerServicePool struct { var ChannelName = "RPCCallerServiceChannel" -func NewRPCCallerServiceChannel(cfg RPCCallerServiceChannelConfigs) *rpcCallerServicePool { +func NewRPCCallerChannel(cfg RPCCallerServiceChannelConfigs) *rpcCallerServicePool { pool := pond.New(cfg.MaxBufferSize, cfg.MaxWorkers, pond.Strategy(pond.Balanced())) return &rpcCallerServicePool{ Pool: pool, diff --git a/internal/tss/channels/rpc_caller_service_channel_test.go b/internal/tss/channels/rpc_caller_channel_test.go similarity index 98% rename from internal/tss/channels/rpc_caller_service_channel_test.go rename to internal/tss/channels/rpc_caller_channel_test.go index a2a7fa2..f965536 100644 --- a/internal/tss/channels/rpc_caller_service_channel_test.go +++ b/internal/tss/channels/rpc_caller_channel_test.go @@ -31,7 +31,7 @@ func TestSend(t *testing.T) { MaxBufferSize: 10, MaxWorkers: 10, } - channel := NewRPCCallerServiceChannel(cfgs) + channel := NewRPCCallerChannel(cfgs) payload := tss.Payload{} payload.WebhookURL = "www.stellar.com" payload.TransactionHash = "hash" @@ -74,7 +74,7 @@ func TestReceivee(t *testing.T) { MaxBufferSize: 10, MaxWorkers: 10, } - channel := NewRPCCallerServiceChannel(cfgs) + channel := NewRPCCallerChannel(cfgs) payload := tss.Payload{} payload.WebhookURL = "www.stellar.com" payload.TransactionHash = "hash" From 16581fdbfe5ceb32162324c32d622abb224c0848 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Wed, 25 Sep 2024 20:48:20 -0700 Subject: [PATCH 066/113] remove commented code --- .../tss/channels/rpc_caller_channel_test.go | 147 ------------------ 1 file changed, 147 deletions(-) diff --git a/internal/tss/channels/rpc_caller_channel_test.go b/internal/tss/channels/rpc_caller_channel_test.go index f965536..c2f4590 100644 --- a/internal/tss/channels/rpc_caller_channel_test.go +++ b/internal/tss/channels/rpc_caller_channel_test.go @@ -128,150 +128,3 @@ func TestReceivee(t *testing.T) { }) } - -/* -func TestReceive(t *testing.T) { - dbt := dbtest.Open(t) - defer dbt.Close() - - dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) - require.NoError(t, err) - defer dbConnectionPool.Close() - store := store.NewStore(dbConnectionPool) - txServiceMock := services.TransactionServiceMock{} - defer txServiceMock.AssertExpectations(t) - routerMock := router.MockRouter{} - defer routerMock.AssertExpectations(t) - rpcServiceMock := services.RPCServiceMock{} - defer rpcServiceMock.AssertExpectations(t) - txManager := services.NewTransactionManager(services.TransactionManagerConfigs{ - TxService: &txServiceMock, - RPCService: &rpcServiceMock, - Store: store, - }) - cfgs := RPCCallerServiceChannelConfigs{ - Store: store, - TxManager: txManager, - Router: &routerMock, - MaxBufferSize: 1, - MaxWorkers: 1, - } - networkPass := "passphrase" - channel := NewRPCCallerServiceChannel(cfgs) - feeBumpTx := utils.BuildTestFeeBumpTransaction() - feeBumpTxXDR, _ := feeBumpTx.Base64() - feeBumpTxHash, _ := feeBumpTx.HashHex(networkPass) - payload := tss.Payload{} - payload.WebhookURL = "www.stellar.com" - payload.TransactionHash = "hash" - payload.TransactionXDR = "xdr" - - t.Run("fail_on_tx_build_and_sign", func(t *testing.T) { - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(nil, errors.New("signing failed")). - Once() - channel.Receive(payload) - - var status string - err = dbConnectionPool.GetContext(context.Background(), &status, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, string(tss.NewStatus), status) - }) - - t.Run("sign_and_submit_tx_fails", func(t *testing.T) { - sendResp := tss.RPCSendTxResponse{} - sendResp.Code.OtherCodes = tss.RPCFailCode - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(feeBumpTx, nil). - Once(). - On("NetworkPassphrase"). - Return(networkPass). - Once(). - On("SendTransaction", feeBumpTxXDR). - Return(sendResp, errors.New("RPC Fail")). - Once() - - channel.Receive(payload) - - var txStatus string - err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, txStatus, string(tss.NewStatus)) - - var tryStatus int - feeBumpTxHash, _ := feeBumpTx.HashHex(networkPass) - err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) - require.NoError(t, err) - assert.Equal(t, int(tss.RPCFailCode), tryStatus) - - }) - - t.Run("routes_payload", func(t *testing.T) { - sendResp := tss.RPCSendTxResponse{} - sendResp.Status = tss.ErrorStatus - sendResp.TransactionHash = feeBumpTxHash - sendResp.TransactionXDR = feeBumpTxXDR - sendResp.Code.TxResultCode = xdr.TransactionResultCodeTxTooEarly - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(feeBumpTx, nil). - Once(). - On("NetworkPassphrase"). - Return(networkPass). - Once(). - On("SendTransaction", feeBumpTxXDR). - Return(sendResp, nil). - Once() - routerMock. - On("Route", mock.AnythingOfType("tss.Payload")). - Return(). - Once() - - channel.Receive(payload) - - var txStatus string - err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, string(tss.ErrorStatus), txStatus) - - var tryStatus int - err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) - require.NoError(t, err) - assert.Equal(t, int(xdr.TransactionResultCodeTxTooEarly), tryStatus) - }) - - t.Run("does_not_routes_payload", func(t *testing.T) { - sendResp := tss.RPCSendTxResponse{} - sendResp.Status = tss.PendingStatus - sendResp.TransactionHash = feeBumpTxHash - sendResp.TransactionXDR = feeBumpTxXDR - sendResp.Code.TxResultCode = xdr.TransactionResultCodeTxSuccess - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(feeBumpTx, nil). - Once(). - On("NetworkPassphrase"). - Return(networkPass). - Once(). - On("SendTransaction", feeBumpTxXDR). - Return(sendResp, nil). - Once() - // this time the router mock is not called - - channel.Receive(payload) - - var txStatus string - err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, string(tss.PendingStatus), txStatus) - - var tryStatus int - err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) - require.NoError(t, err) - assert.Equal(t, int(xdr.TransactionResultCodeTxSuccess), tryStatus) - }) - -} -*/ From ab7c0d7ed9ab2798aff9cc96b00e5b5132814b96 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Wed, 25 Sep 2024 21:38:04 -0700 Subject: [PATCH 067/113] moving the mocks file inside servicesmocks dir --- .../{mocks.go => servicesmocks/rpc_service_mocks.go} | 5 +++-- internal/tss/services/transaction_manager_test.go | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) rename internal/services/{mocks.go => servicesmocks/rpc_service_mocks.go} (83%) diff --git a/internal/services/mocks.go b/internal/services/servicesmocks/rpc_service_mocks.go similarity index 83% rename from internal/services/mocks.go rename to internal/services/servicesmocks/rpc_service_mocks.go index 255c955..f72c348 100644 --- a/internal/services/mocks.go +++ b/internal/services/servicesmocks/rpc_service_mocks.go @@ -1,7 +1,8 @@ -package services +package servicesmocks import ( "github.com/stellar/wallet-backend/internal/entities" + "github.com/stellar/wallet-backend/internal/services" "github.com/stretchr/testify/mock" ) @@ -9,7 +10,7 @@ type RPCServiceMock struct { mock.Mock } -var _ RPCService = (*RPCServiceMock)(nil) +var _ services.RPCService = (*RPCServiceMock)(nil) func (r *RPCServiceMock) SendTransaction(transactionXdr string) (entities.RPCSendTransactionResult, error) { args := r.Called(transactionXdr) diff --git a/internal/tss/services/transaction_manager_test.go b/internal/tss/services/transaction_manager_test.go index ff64eee..5ed8d22 100644 --- a/internal/tss/services/transaction_manager_test.go +++ b/internal/tss/services/transaction_manager_test.go @@ -9,7 +9,7 @@ import ( "github.com/stellar/wallet-backend/internal/db" "github.com/stellar/wallet-backend/internal/db/dbtest" "github.com/stellar/wallet-backend/internal/entities" - "github.com/stellar/wallet-backend/internal/services" + "github.com/stellar/wallet-backend/internal/services/servicesmocks" "github.com/stellar/wallet-backend/internal/tss" "github.com/stellar/wallet-backend/internal/tss/store" "github.com/stellar/wallet-backend/internal/tss/utils" @@ -26,7 +26,7 @@ func TestBuildAndSubmitTransaction(t *testing.T) { defer dbConnectionPool.Close() store := store.NewStore(dbConnectionPool) txServiceMock := TransactionServiceMock{} - rpcServiceMock := services.RPCServiceMock{} + rpcServiceMock := servicesmocks.RPCServiceMock{} txManager := NewTransactionManager(TransactionManagerConfigs{ TxService: &txServiceMock, RPCService: &rpcServiceMock, From 5fe5d0b79e4bb578ca5132fdb3ef6bfd9766fcc7 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Thu, 26 Sep 2024 00:58:30 -0700 Subject: [PATCH 068/113] name changes --- internal/serve/serve.go | 2 +- internal/tss/channels/rpc_caller_channel.go | 26 +++++++++---------- .../tss/channels/rpc_caller_channel_test.go | 12 ++++----- 3 files changed, 20 insertions(+), 20 deletions(-) diff --git a/internal/serve/serve.go b/internal/serve/serve.go index 23c46d9..c960bd5 100644 --- a/internal/serve/serve.go +++ b/internal/serve/serve.go @@ -191,7 +191,7 @@ func initHandlerDeps(cfg Configs) (handlerDeps, error) { RPCService: rpcService, Store: store, }) - tssChannelConfigs := tsschannel.RPCCallerServiceChannelConfigs{ + tssChannelConfigs := tsschannel.RPCCallerChannelConfigs{ TxManager: txManager, Store: store, MaxBufferSize: cfg.RPCCallerServiceChannelBufferSize, diff --git a/internal/tss/channels/rpc_caller_channel.go b/internal/tss/channels/rpc_caller_channel.go index 5d8e036..9a36318 100644 --- a/internal/tss/channels/rpc_caller_channel.go +++ b/internal/tss/channels/rpc_caller_channel.go @@ -13,7 +13,7 @@ import ( "github.com/stellar/wallet-backend/internal/tss/store" ) -type RPCCallerServiceChannelConfigs struct { +type RPCCallerChannelConfigs struct { TxManager services.TransactionManager Router router.Router Store store.Store @@ -21,18 +21,18 @@ type RPCCallerServiceChannelConfigs struct { MaxWorkers int } -type rpcCallerServicePool struct { +type rpcCallerPool struct { Pool *pond.WorkerPool TxManager services.TransactionManager Router router.Router Store store.Store } -var ChannelName = "RPCCallerServiceChannel" +var RPCCallerChannelName = "RPCCallerChannel" -func NewRPCCallerChannel(cfg RPCCallerServiceChannelConfigs) *rpcCallerServicePool { +func NewRPCCallerChannel(cfg RPCCallerChannelConfigs) *rpcCallerPool { pool := pond.New(cfg.MaxBufferSize, cfg.MaxWorkers, pond.Strategy(pond.Balanced())) - return &rpcCallerServicePool{ + return &rpcCallerPool{ Pool: pool, TxManager: cfg.TxManager, Store: cfg.Store, @@ -41,41 +41,41 @@ func NewRPCCallerChannel(cfg RPCCallerServiceChannelConfigs) *rpcCallerServicePo } -func (p *rpcCallerServicePool) Send(payload tss.Payload) { +func (p *rpcCallerPool) Send(payload tss.Payload) { p.Pool.Submit(func() { p.Receive(payload) }) } -func (p *rpcCallerServicePool) Receive(payload tss.Payload) { +func (p *rpcCallerPool) Receive(payload tss.Payload) { ctx := context.Background() // Create a new transaction record in the transactions table. err := p.Store.UpsertTransaction(ctx, payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.RPCTXStatus{OtherStatus: tss.NewStatus}) if err != nil { - log.Errorf("RPCCallerChannel: Unable to upsert transaction into transactions table: %e", err) + log.Errorf("%s: Unable to upsert transaction into transactions table: %e", RPCCallerChannelName, err) return } - rpcSendResp, err := p.TxManager.BuildAndSubmitTransaction(ctx, ChannelName, payload) + rpcSendResp, err := p.TxManager.BuildAndSubmitTransaction(ctx, RPCCallerChannelName, payload) if err != nil { - log.Errorf("RPCCallerChannel: Unable to sign and submit transaction: %e", err) + log.Errorf("%s: Unable to sign and submit transaction: %e", RPCCallerChannelName, err) return } payload.RpcSubmitTxResponse = rpcSendResp if rpcSendResp.Status.RPCStatus == entities.TryAgainLaterStatus || rpcSendResp.Status.RPCStatus == entities.ErrorStatus { err = p.Router.Route(payload) if err != nil { - log.Errorf("RPCCallerChannel: Unable to route payload: %e", err) + log.Errorf("%s: Unable to route payload: %e", RPCCallerChannelName, err) } } } -func (p *rpcCallerServicePool) SetRouter(router router.Router) { +func (p *rpcCallerPool) SetRouter(router router.Router) { p.Router = router } -func (p *rpcCallerServicePool) Stop() { +func (p *rpcCallerPool) Stop() { p.Pool.StopAndWait() } diff --git a/internal/tss/channels/rpc_caller_channel_test.go b/internal/tss/channels/rpc_caller_channel_test.go index c2f4590..b5899c0 100644 --- a/internal/tss/channels/rpc_caller_channel_test.go +++ b/internal/tss/channels/rpc_caller_channel_test.go @@ -24,7 +24,7 @@ func TestSend(t *testing.T) { store := store.NewStore(dbConnectionPool) txManagerMock := services.TransactionManagerMock{} routerMock := router.MockRouter{} - cfgs := RPCCallerServiceChannelConfigs{ + cfgs := RPCCallerChannelConfigs{ Store: store, TxManager: &txManagerMock, Router: &routerMock, @@ -43,7 +43,7 @@ func TestSend(t *testing.T) { payload.RpcSubmitTxResponse = rpcResp txManagerMock. - On("BuildAndSubmitTransaction", context.Background(), ChannelName, payload). + On("BuildAndSubmitTransaction", context.Background(), RPCCallerChannelName, payload). Return(rpcResp, nil). Once() @@ -67,7 +67,7 @@ func TestReceivee(t *testing.T) { store := store.NewStore(dbConnectionPool) txManagerMock := services.TransactionManagerMock{} routerMock := router.MockRouter{} - cfgs := RPCCallerServiceChannelConfigs{ + cfgs := RPCCallerChannelConfigs{ Store: store, TxManager: &txManagerMock, Router: &routerMock, @@ -82,7 +82,7 @@ func TestReceivee(t *testing.T) { t.Run("build_and_submit_tx_fail", func(t *testing.T) { txManagerMock. - On("BuildAndSubmitTransaction", context.Background(), ChannelName, payload). + On("BuildAndSubmitTransaction", context.Background(), RPCCallerChannelName, payload). Return(tss.RPCSendTxResponse{}, errors.New("build tx failed")). Once() @@ -98,7 +98,7 @@ func TestReceivee(t *testing.T) { payload.RpcSubmitTxResponse = rpcResp txManagerMock. - On("BuildAndSubmitTransaction", context.Background(), ChannelName, payload). + On("BuildAndSubmitTransaction", context.Background(), RPCCallerChannelName, payload). Return(rpcResp, nil). Once() @@ -113,7 +113,7 @@ func TestReceivee(t *testing.T) { payload.RpcSubmitTxResponse = rpcResp txManagerMock. - On("BuildAndSubmitTransaction", context.Background(), ChannelName, payload). + On("BuildAndSubmitTransaction", context.Background(), RPCCallerChannelName, payload). Return(rpcResp, nil). Once() From 76df04bc02c5a4ddc8d14efb9aae5dc49bc41d1d Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Thu, 26 Sep 2024 01:04:33 -0700 Subject: [PATCH 069/113] refactor --- internal/entities/rpc.go | 58 ++ internal/services/rpc_service.go | 101 +++ .../servicesmocks/rpc_service_mocks.go | 27 + ...handler_service_non_jitter_channel_test.go | 224 ------- ...ter_channel.go => error_jitter_channel.go} | 41 +- .../tss/channels/error_jitter_channel_test.go | 141 +++++ ...channel.go => error_non_jitter_channel.go} | 39 +- .../channels/error_non_jitter_channel_test.go | 140 ++++ ...ror_service_handler_jitter_channel_test.go | 224 ------- internal/tss/channels/rpc_caller_channel.go | 81 +++ .../tss/channels/rpc_caller_channel_test.go | 130 ++++ .../channels/rpc_caller_service_channel.go | 78 --- .../rpc_caller_service_channel_test.go | 207 ------ internal/tss/channels/utils.go | 46 -- internal/tss/channels/utils_test.go | 143 ----- internal/tss/router/mocks.go | 5 +- internal/tss/router/router.go | 77 +-- internal/tss/router/router_test.go | 129 +++- .../tss/services/error_handler_service.go | 36 -- .../services/error_handler_service_test.go | 54 -- internal/tss/services/mocks.go | 43 +- internal/tss/services/rpc_caller_service.go | 21 - internal/tss/services/transaction_manager.go | 76 +++ .../tss/services/transaction_manager_test.go | 195 ++++++ .../transaction_service.go | 113 +--- .../tss/services/transaction_service_test.go | 236 +++++++ internal/tss/services/types.go | 7 - .../tss/services/webhook_handler_service.go | 19 - internal/tss/store/store.go | 2 +- internal/tss/store/store_test.go | 9 +- internal/tss/types.go | 149 +++-- internal/tss/utils/mocks.go | 50 -- .../tss/utils/transaction_service_test.go | 596 ------------------ internal/utils/http_client.go | 21 + 34 files changed, 1553 insertions(+), 1965 deletions(-) create mode 100644 internal/entities/rpc.go create mode 100644 internal/services/rpc_service.go create mode 100644 internal/services/servicesmocks/rpc_service_mocks.go delete mode 100644 internal/tss/channels/error_handler_service_non_jitter_channel_test.go rename internal/tss/channels/{error_handler_service_jitter_channel.go => error_jitter_channel.go} (59%) create mode 100644 internal/tss/channels/error_jitter_channel_test.go rename internal/tss/channels/{error_handler_service_non_jitter_channel.go => error_non_jitter_channel.go} (58%) create mode 100644 internal/tss/channels/error_non_jitter_channel_test.go delete mode 100644 internal/tss/channels/error_service_handler_jitter_channel_test.go create mode 100644 internal/tss/channels/rpc_caller_channel.go create mode 100644 internal/tss/channels/rpc_caller_channel_test.go delete mode 100644 internal/tss/channels/rpc_caller_service_channel.go delete mode 100644 internal/tss/channels/rpc_caller_service_channel_test.go delete mode 100644 internal/tss/channels/utils.go delete mode 100644 internal/tss/channels/utils_test.go delete mode 100644 internal/tss/services/error_handler_service.go delete mode 100644 internal/tss/services/error_handler_service_test.go delete mode 100644 internal/tss/services/rpc_caller_service.go create mode 100644 internal/tss/services/transaction_manager.go create mode 100644 internal/tss/services/transaction_manager_test.go rename internal/tss/{utils => services}/transaction_service.go (50%) create mode 100644 internal/tss/services/transaction_service_test.go delete mode 100644 internal/tss/services/types.go delete mode 100644 internal/tss/services/webhook_handler_service.go delete mode 100644 internal/tss/utils/mocks.go delete mode 100644 internal/tss/utils/transaction_service_test.go create mode 100644 internal/utils/http_client.go diff --git a/internal/entities/rpc.go b/internal/entities/rpc.go new file mode 100644 index 0000000..eb252bb --- /dev/null +++ b/internal/entities/rpc.go @@ -0,0 +1,58 @@ +package entities + +import ( + "encoding/json" +) + +type RPCStatus string + +const ( + // sendTransaction statuses + PendingStatus RPCStatus = "PENDING" + DuplicateStatus RPCStatus = "DUPLICATE" + TryAgainLaterStatus RPCStatus = "TRY_AGAIN_LATER" + ErrorStatus RPCStatus = "ERROR" + // getTransaction statuses + NotFoundStatus RPCStatus = "NOT_FOUND" + FailedStatus RPCStatus = "FAILED" + SuccessStatus RPCStatus = "SUCCESS" +) + +type RPCEntry struct { + Key string `json:"key"` + XDR string `json:"xdr"` + LastModifiedLedgerSeq int64 `json:"lastModifiedLedgerSeq"` +} + +type RPCResponse struct { + Result json.RawMessage `json:"result"` + JSONRPC string `json:"jsonrpc"` + ID int64 `json:"id"` +} + +type RPCGetLedgerEntriesResult struct { + Entries []RPCEntry `json:"entries"` +} + +type RPCGetTransactionResult struct { + Status string `json:"status"` + LatestLedger int64 `json:"latestLedger"` + LatestLedgerCloseTime string `json:"latestLedgerCloseTime"` + OldestLedger string `json:"oldestLedger"` + OldestLedgerCloseTime string `json:"oldestLedgerCloseTime"` + ApplicationOrder string `json:"applicationOrder"` + EnvelopeXDR string `json:"envelopeXdr"` + ResultXDR string `json:"resultXdr"` + ResultMetaXDR string `json:"resultMetaXdr"` + Ledger string `json:"ledger"` + CreatedAt string `json:"createdAt"` + ErrorResultXDR string `json:"errorResultXdr"` +} + +type RPCSendTransactionResult struct { + Status string `json:"status"` + LatestLedger int64 `json:"latestLedger"` + LatestLedgerCloseTime string `json:"latestLedgerCloseTime"` + Hash string `json:"hash"` + ErrorResultXDR string `json:"errorResultXdr"` +} diff --git a/internal/services/rpc_service.go b/internal/services/rpc_service.go new file mode 100644 index 0000000..da10b93 --- /dev/null +++ b/internal/services/rpc_service.go @@ -0,0 +1,101 @@ +package services + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + + "github.com/stellar/wallet-backend/internal/entities" + "github.com/stellar/wallet-backend/internal/utils" +) + +type RPCService interface { + GetTransaction(transactionHash string) (entities.RPCGetTransactionResult, error) + SendTransaction(transactionXDR string) (entities.RPCSendTransactionResult, error) +} + +type rpcService struct { + rpcURL string + httpClient utils.HTTPClient +} + +var _ RPCService = (*rpcService)(nil) + +func NewRPCService(rpcURL string, httpClient utils.HTTPClient) (*rpcService, error) { + if rpcURL == "" { + return nil, errors.New("rpcURL cannot be nil") + } + if httpClient == nil { + return nil, errors.New("httpClient cannot be nil") + } + + return &rpcService{ + rpcURL: rpcURL, + httpClient: httpClient, + }, nil +} + +func (r *rpcService) GetTransaction(transactionHash string) (entities.RPCGetTransactionResult, error) { + resultBytes, err := r.sendRPCRequest("getTransaction", map[string]string{"hash": transactionHash}) + if err != nil { + return entities.RPCGetTransactionResult{}, fmt.Errorf("sending getTransaction request: %w", err) + } + + var result entities.RPCGetTransactionResult + err = json.Unmarshal(resultBytes, &result) + if err != nil { + return entities.RPCGetTransactionResult{}, fmt.Errorf("parsing getTransaction result JSON: %w", err) + } + + return result, nil +} + +func (r *rpcService) SendTransaction(transactionXDR string) (entities.RPCSendTransactionResult, error) { + resultBytes, err := r.sendRPCRequest("sendTransaction", map[string]string{"transaction": transactionXDR}) + if err != nil { + return entities.RPCSendTransactionResult{}, fmt.Errorf("sending sendTransaction request: %w", err) + } + + var result entities.RPCSendTransactionResult + err = json.Unmarshal(resultBytes, &result) + if err != nil { + return entities.RPCSendTransactionResult{}, fmt.Errorf("parsing sendTransaction result JSON: %w", err) + } + + return result, nil +} + +func (r *rpcService) sendRPCRequest(method string, params map[string]string) (json.RawMessage, error) { + payload := map[string]interface{}{ + "jsonrpc": "2.0", + "id": 1, + "method": method, + "params": params, + } + jsonData, err := json.Marshal(payload) + + if err != nil { + return nil, fmt.Errorf("marshaling payload") + } + + resp, err := r.httpClient.Post(r.rpcURL, "application/json", bytes.NewBuffer(jsonData)) + if err != nil { + return nil, fmt.Errorf("sending POST request to RPC: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("unmarshaling RPC response: %w", err) + } + + var res entities.RPCResponse + err = json.Unmarshal(body, &res) + if err != nil { + return nil, fmt.Errorf("parsing RPC response JSON: %w", err) + } + + return res.Result, nil +} diff --git a/internal/services/servicesmocks/rpc_service_mocks.go b/internal/services/servicesmocks/rpc_service_mocks.go new file mode 100644 index 0000000..f72c348 --- /dev/null +++ b/internal/services/servicesmocks/rpc_service_mocks.go @@ -0,0 +1,27 @@ +package servicesmocks + +import ( + "github.com/stellar/wallet-backend/internal/entities" + "github.com/stellar/wallet-backend/internal/services" + "github.com/stretchr/testify/mock" +) + +type RPCServiceMock struct { + mock.Mock +} + +var _ services.RPCService = (*RPCServiceMock)(nil) + +func (r *RPCServiceMock) SendTransaction(transactionXdr string) (entities.RPCSendTransactionResult, error) { + args := r.Called(transactionXdr) + return args.Get(0).(entities.RPCSendTransactionResult), args.Error(1) +} + +func (r *RPCServiceMock) GetTransaction(transactionHash string) (entities.RPCGetTransactionResult, error) { + args := r.Called(transactionHash) + return args.Get(0).(entities.RPCGetTransactionResult), args.Error(1) +} + +type TransactionManagerMock struct { + mock.Mock +} diff --git a/internal/tss/channels/error_handler_service_non_jitter_channel_test.go b/internal/tss/channels/error_handler_service_non_jitter_channel_test.go deleted file mode 100644 index 51eae3d..0000000 --- a/internal/tss/channels/error_handler_service_non_jitter_channel_test.go +++ /dev/null @@ -1,224 +0,0 @@ -package channels - -import ( - "context" - "errors" - "testing" - - "github.com/stellar/go/xdr" - "github.com/stellar/wallet-backend/internal/db" - "github.com/stellar/wallet-backend/internal/db/dbtest" - "github.com/stellar/wallet-backend/internal/tss" - "github.com/stellar/wallet-backend/internal/tss/router" - "github.com/stellar/wallet-backend/internal/tss/store" - "github.com/stellar/wallet-backend/internal/tss/utils" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" -) - -func TestNonJitterSend(t *testing.T) { - dbt := dbtest.Open(t) - defer dbt.Close() - - dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) - require.NoError(t, err) - defer dbConnectionPool.Close() - store := store.NewStore(dbConnectionPool) - txServiceMock := utils.TransactionServiceMock{} - cfg := RPCErrorHandlerServiceNonJitterChannelConfigs{ - Store: store, - TxService: &txServiceMock, - MaxBufferSize: 1, - MaxWorkers: 1, - MaxRetries: 3, - WaitBtwnRetriesMS: 10, - } - channel := NewErrorHandlerServiceNonJitterChannel(cfg) - - payload := tss.Payload{} - payload.WebhookURL = "www.stellar.com" - payload.TransactionHash = "hash" - payload.TransactionXDR = "xdr" - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(nil, errors.New("signing failed")) - - _ = store.UpsertTransaction(context.Background(), payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.NewStatus) - - channel.Send(payload) - channel.Stop() - - var status string - err = dbConnectionPool.GetContext(context.Background(), &status, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, status, string(tss.NewStatus)) -} - -func TestNonJitterReceive(t *testing.T) { - dbt := dbtest.Open(t) - defer dbt.Close() - - dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) - require.NoError(t, err) - defer dbConnectionPool.Close() - store := store.NewStore(dbConnectionPool) - txServiceMock := utils.TransactionServiceMock{} - cfg := RPCErrorHandlerServiceNonJitterChannelConfigs{ - Store: store, - TxService: &txServiceMock, - MaxBufferSize: 1, - MaxWorkers: 1, - MaxRetries: 3, - WaitBtwnRetriesMS: 10, - } - channel := NewErrorHandlerServiceNonJitterChannel(cfg) - - mockRouter := router.MockRouter{} - defer mockRouter.AssertExpectations(t) - channel.SetRouter(&mockRouter) - networkPass := "passphrase" - feeBumpTx := utils.BuildTestFeeBumpTransaction() - feeBumpTxXDR, _ := feeBumpTx.Base64() - feeBumpTxHash, _ := feeBumpTx.HashHex(networkPass) - payload := tss.Payload{} - payload.WebhookURL = "www.stellar.com" - payload.TransactionHash = "hash" - payload.TransactionXDR = "xdr" - - _ = store.UpsertTransaction(context.Background(), payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.NewStatus) - - t.Run("signing_and_submitting_tx_fails", func(t *testing.T) { - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(nil, errors.New("sign tx failed")). - Once() - - channel.Receive(payload) - - var txStatus string - err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, string(tss.NewStatus), txStatus) - - }) - t.Run("payload_gets_routed", func(t *testing.T) { - sendResp := tss.RPCSendTxResponse{} - sendResp.Status = tss.TryAgainLaterStatus - sendResp.TransactionHash = feeBumpTxHash - sendResp.TransactionXDR = feeBumpTxXDR - sendResp.Code.TxResultCode = xdr.TransactionResultCodeTxInsufficientFee - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(feeBumpTx, nil). - Once(). - On("NetworkPassphrase"). - Return(networkPass). - Once(). - On("SendTransaction", feeBumpTxXDR). - Return(sendResp, nil). - Once() - - mockRouter. - On("Route", mock.AnythingOfType("tss.Payload")). - Return(). - Once() - - channel.Receive(payload) - - var txStatus string - err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, string(tss.TryAgainLaterStatus), txStatus) - - var tryStatus int - err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) - require.NoError(t, err) - assert.Equal(t, int(xdr.TransactionResultCodeTxInsufficientFee), tryStatus) - }) - - t.Run("retries", func(t *testing.T) { - sendResp1 := tss.RPCSendTxResponse{} - sendResp1.Status = tss.ErrorStatus - sendResp1.TransactionHash = feeBumpTxHash - sendResp1.TransactionXDR = feeBumpTxXDR - sendResp1.Code.TxResultCode = xdr.TransactionResultCodeTxTooEarly - - sendResp2 := tss.RPCSendTxResponse{} - sendResp2.Status = tss.TryAgainLaterStatus - sendResp2.TransactionHash = feeBumpTxHash - sendResp2.TransactionXDR = feeBumpTxXDR - sendResp2.Code.TxResultCode = xdr.TransactionResultCodeTxInsufficientFee - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(feeBumpTx, nil). - Twice(). - On("NetworkPassphrase"). - Return(networkPass). - Twice() - - txServiceMock. - On("SendTransaction", feeBumpTxXDR). - Return(sendResp1, nil). - Once() - - txServiceMock. - On("SendTransaction", feeBumpTxXDR). - Return(sendResp2, nil). - Once() - - mockRouter. - On("Route", mock.AnythingOfType("tss.Payload")). - Return(). - Once() - - channel.Receive(payload) - - var txStatus string - err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, string(tss.TryAgainLaterStatus), txStatus) - - var tryStatus int - err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) - require.NoError(t, err) - assert.Equal(t, int(xdr.TransactionResultCodeTxInsufficientFee), tryStatus) - }) - - t.Run("max_retries", func(t *testing.T) { - sendResp := tss.RPCSendTxResponse{} - sendResp.Status = tss.ErrorStatus - sendResp.TransactionHash = feeBumpTxHash - sendResp.TransactionXDR = feeBumpTxXDR - sendResp.Code.TxResultCode = xdr.TransactionResultCodeTxTooEarly - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(feeBumpTx, nil). - Times(3). - On("NetworkPassphrase"). - Return(networkPass). - Times(3) - - txServiceMock. - On("SendTransaction", feeBumpTxXDR). - Return(sendResp, nil). - Times(3) - - mockRouter. - On("Route", mock.AnythingOfType("tss.Payload")). - Return(). - Once() - - channel.Receive(payload) - - var txStatus string - err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, string(tss.ErrorStatus), txStatus) - - var tryStatus int - err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) - require.NoError(t, err) - assert.Equal(t, int(xdr.TransactionResultCodeTxTooEarly), tryStatus) - }) -} diff --git a/internal/tss/channels/error_handler_service_jitter_channel.go b/internal/tss/channels/error_jitter_channel.go similarity index 59% rename from internal/tss/channels/error_handler_service_jitter_channel.go rename to internal/tss/channels/error_jitter_channel.go index a13c442..cd38fb8 100644 --- a/internal/tss/channels/error_handler_service_jitter_channel.go +++ b/internal/tss/channels/error_jitter_channel.go @@ -9,14 +9,12 @@ import ( "github.com/stellar/go/support/log" "github.com/stellar/wallet-backend/internal/tss" "github.com/stellar/wallet-backend/internal/tss/router" - tss_store "github.com/stellar/wallet-backend/internal/tss/store" - "github.com/stellar/wallet-backend/internal/tss/utils" + "github.com/stellar/wallet-backend/internal/tss/services" "golang.org/x/exp/rand" ) -type RPCErrorHandlerServiceJitterChannelConfigs struct { - Store tss_store.Store - TxService utils.TransactionService +type ErrorJitterChannelConfigs struct { + TxManager services.TransactionManager Router router.Router MaxBufferSize int MaxWorkers int @@ -24,52 +22,57 @@ type RPCErrorHandlerServiceJitterChannelConfigs struct { MinWaitBtwnRetriesMS int } -type rpcErrorHandlerServiceJitterPool struct { +type errorJitterPool struct { Pool *pond.WorkerPool - TxService utils.TransactionService - Store tss_store.Store + TxManager services.TransactionManager Router router.Router MaxRetries int MinWaitBtwnRetriesMS int } +var ErrorJitterChannelName = "ErrorJitterChannel" + func jitter(dur time.Duration) time.Duration { halfDur := int64(dur / 2) delta := rand.Int63n(halfDur) - halfDur/2 return dur + time.Duration(delta) } -func NewErrorHandlerServiceJitterChannel(cfg RPCErrorHandlerServiceJitterChannelConfigs) *rpcErrorHandlerServiceJitterPool { +func NewErrorJitterChannel(cfg ErrorJitterChannelConfigs) *errorJitterPool { pool := pond.New(cfg.MaxBufferSize, cfg.MaxWorkers, pond.Strategy(pond.Balanced())) - return &rpcErrorHandlerServiceJitterPool{ + return &errorJitterPool{ Pool: pool, - TxService: cfg.TxService, - Store: cfg.Store, + TxManager: cfg.TxManager, + Router: cfg.Router, MaxRetries: cfg.MaxRetries, MinWaitBtwnRetriesMS: cfg.MinWaitBtwnRetriesMS, } } -func (p *rpcErrorHandlerServiceJitterPool) Send(payload tss.Payload) { +func (p *errorJitterPool) Send(payload tss.Payload) { p.Pool.Submit(func() { p.Receive(payload) }) } -func (p *rpcErrorHandlerServiceJitterPool) Receive(payload tss.Payload) { +func (p *errorJitterPool) Receive(payload tss.Payload) { ctx := context.Background() var i int for i = 0; i < p.MaxRetries; i++ { currentBackoff := p.MinWaitBtwnRetriesMS * (1 << i) time.Sleep(jitter(time.Duration(currentBackoff)) * time.Microsecond) - rpcSendResp, err := BuildAndSubmitTransaction(ctx, "ErrorHandlerServiceJitterChannel", payload, p.Store, p.TxService) + rpcSendResp, err := p.TxManager.BuildAndSubmitTransaction(ctx, ErrorJitterChannelName, payload) if err != nil { - log.Errorf(err.Error()) + log.Errorf("%s: Unable to sign and submit transaction: %e", ErrorJitterChannelName, err) return } payload.RpcSubmitTxResponse = rpcSendResp if !slices.Contains(tss.JitterErrorCodes, rpcSendResp.Code.TxResultCode) { - p.Router.Route(payload) + err = p.Router.Route(payload) + if err != nil { + log.Errorf("%s: Unable to route payload: %e", ErrorJitterChannelName, err) + return + } return } } @@ -80,10 +83,10 @@ func (p *rpcErrorHandlerServiceJitterPool) Receive(payload tss.Payload) { } } -func (p *rpcErrorHandlerServiceJitterPool) SetRouter(router router.Router) { +func (p *errorJitterPool) SetRouter(router router.Router) { p.Router = router } -func (p *rpcErrorHandlerServiceJitterPool) Stop() { +func (p *errorJitterPool) Stop() { p.Pool.StopAndWait() } diff --git a/internal/tss/channels/error_jitter_channel_test.go b/internal/tss/channels/error_jitter_channel_test.go new file mode 100644 index 0000000..8dd746d --- /dev/null +++ b/internal/tss/channels/error_jitter_channel_test.go @@ -0,0 +1,141 @@ +package channels + +import ( + "context" + "errors" + "testing" + + "github.com/stellar/wallet-backend/internal/db" + "github.com/stellar/wallet-backend/internal/db/dbtest" + "github.com/stellar/wallet-backend/internal/entities" + "github.com/stellar/wallet-backend/internal/tss" + "github.com/stellar/wallet-backend/internal/tss/router" + "github.com/stellar/wallet-backend/internal/tss/services" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestJitterSend(t *testing.T) { + dbt := dbtest.Open(t) + defer dbt.Close() + dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) + require.NoError(t, err) + defer dbConnectionPool.Close() + txManagerMock := services.TransactionManagerMock{} + routerMock := router.MockRouter{} + cfg := ErrorJitterChannelConfigs{ + TxManager: &txManagerMock, + Router: &routerMock, + MaxBufferSize: 1, + MaxWorkers: 1, + MaxRetries: 3, + MinWaitBtwnRetriesMS: 10, + } + + channel := NewErrorJitterChannel(cfg) + + payload := tss.Payload{} + payload.WebhookURL = "www.stellar.com" + payload.TransactionHash = "hash" + payload.TransactionXDR = "xdr" + + rpcResp := tss.RPCSendTxResponse{ + Status: tss.RPCTXStatus{RPCStatus: entities.ErrorStatus}, + Code: tss.RPCTXCode{TxResultCode: tss.NonJitterErrorCodes[0]}, + } + payload.RpcSubmitTxResponse = rpcResp + + txManagerMock. + On("BuildAndSubmitTransaction", context.Background(), ErrorJitterChannelName, payload). + Return(rpcResp, nil). + Once() + + routerMock. + On("Route", payload). + Return(nil). + Once() + + channel.Send(payload) + channel.Stop() + + routerMock.AssertCalled(t, "Route", payload) +} + +func TestJitterReceive(t *testing.T) { + dbt := dbtest.Open(t) + defer dbt.Close() + dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) + require.NoError(t, err) + defer dbConnectionPool.Close() + txManagerMock := services.TransactionManagerMock{} + routerMock := router.MockRouter{} + cfg := ErrorJitterChannelConfigs{ + TxManager: &txManagerMock, + Router: &routerMock, + MaxBufferSize: 1, + MaxWorkers: 1, + MaxRetries: 3, + MinWaitBtwnRetriesMS: 10, + } + + channel := NewErrorJitterChannel(cfg) + + payload := tss.Payload{} + payload.WebhookURL = "www.stellar.com" + payload.TransactionHash = "hash" + payload.TransactionXDR = "xdr" + + t.Run("build_and_submit_tx_fail", func(t *testing.T) { + txManagerMock. + On("BuildAndSubmitTransaction", context.Background(), ErrorJitterChannelName, payload). + Return(tss.RPCSendTxResponse{}, errors.New("build tx failed")). + Once() + + channel.Receive(payload) + + routerMock.AssertNotCalled(t, "Route", payload) + }) + t.Run("retries", func(t *testing.T) { + sendResp1 := tss.RPCSendTxResponse{ + Status: tss.RPCTXStatus{RPCStatus: entities.ErrorStatus}, + Code: tss.RPCTXCode{TxResultCode: tss.JitterErrorCodes[0]}, + } + sendResp2 := tss.RPCSendTxResponse{ + Status: tss.RPCTXStatus{RPCStatus: entities.ErrorStatus}, + Code: tss.RPCTXCode{TxResultCode: tss.NonJitterErrorCodes[0]}, + } + + txManagerMock. + On("BuildAndSubmitTransaction", context.Background(), ErrorJitterChannelName, mock.AnythingOfType("tss.Payload")). + Return(sendResp1, nil). + Once() + txManagerMock. + On("BuildAndSubmitTransaction", context.Background(), ErrorJitterChannelName, mock.AnythingOfType("tss.Payload")). + Return(sendResp2, nil). + Once() + routerMock. + On("Route", mock.AnythingOfType("tss.Payload")). + Return(nil). + Once() + + channel.Receive(payload) + }) + + t.Run("max_retries", func(t *testing.T) { + sendResp := tss.RPCSendTxResponse{ + Status: tss.RPCTXStatus{RPCStatus: entities.ErrorStatus}, + Code: tss.RPCTXCode{TxResultCode: tss.JitterErrorCodes[0]}, + } + + txManagerMock. + On("BuildAndSubmitTransaction", context.Background(), ErrorJitterChannelName, mock.AnythingOfType("tss.Payload")). + Return(sendResp, nil). + Times(3) + routerMock. + On("Route", mock.AnythingOfType("tss.Payload")). + Return(nil). + Once() + + channel.Receive(payload) + }) +} diff --git a/internal/tss/channels/error_handler_service_non_jitter_channel.go b/internal/tss/channels/error_non_jitter_channel.go similarity index 58% rename from internal/tss/channels/error_handler_service_non_jitter_channel.go rename to internal/tss/channels/error_non_jitter_channel.go index 16508b0..5daab12 100644 --- a/internal/tss/channels/error_handler_service_non_jitter_channel.go +++ b/internal/tss/channels/error_non_jitter_channel.go @@ -2,6 +2,7 @@ package channels import ( "context" + "fmt" "slices" "time" @@ -9,13 +10,12 @@ import ( "github.com/stellar/go/support/log" "github.com/stellar/wallet-backend/internal/tss" "github.com/stellar/wallet-backend/internal/tss/router" + "github.com/stellar/wallet-backend/internal/tss/services" tss_store "github.com/stellar/wallet-backend/internal/tss/store" - "github.com/stellar/wallet-backend/internal/tss/utils" ) -type RPCErrorHandlerServiceNonJitterChannelConfigs struct { - Store tss_store.Store - TxService utils.TransactionService +type ErrorNonJitterChannelConfigs struct { + TxManager services.TransactionManager Router router.Router MaxBufferSize int MaxWorkers int @@ -23,45 +23,52 @@ type RPCErrorHandlerServiceNonJitterChannelConfigs struct { WaitBtwnRetriesMS int } -type rpcErrorHandlerServiceNonJitterPool struct { +type errorNonJitterPool struct { Pool *pond.WorkerPool - TxService utils.TransactionService + TxManager services.TransactionManager Store tss_store.Store Router router.Router MaxRetries int WaitBtwnRetriesMS int } -func NewErrorHandlerServiceNonJitterChannel(cfg RPCErrorHandlerServiceNonJitterChannelConfigs) *rpcErrorHandlerServiceNonJitterPool { +var ErrorNonJitterChannelName = "ErrorNonJitterChannel" + +func NewErrorNonJitterChannel(cfg ErrorNonJitterChannelConfigs) *errorNonJitterPool { pool := pond.New(cfg.MaxBufferSize, cfg.MaxWorkers, pond.Strategy(pond.Balanced())) - return &rpcErrorHandlerServiceNonJitterPool{ + return &errorNonJitterPool{ Pool: pool, - TxService: cfg.TxService, - Store: cfg.Store, + TxManager: cfg.TxManager, + Router: cfg.Router, MaxRetries: cfg.MaxRetries, WaitBtwnRetriesMS: cfg.WaitBtwnRetriesMS, } } -func (p *rpcErrorHandlerServiceNonJitterPool) Send(payload tss.Payload) { +func (p *errorNonJitterPool) Send(payload tss.Payload) { p.Pool.Submit(func() { p.Receive(payload) }) } -func (p *rpcErrorHandlerServiceNonJitterPool) Receive(payload tss.Payload) { +func (p *errorNonJitterPool) Receive(payload tss.Payload) { ctx := context.Background() var i int for i = 0; i < p.MaxRetries; i++ { + fmt.Println(i) time.Sleep(time.Duration(p.WaitBtwnRetriesMS) * time.Microsecond) - rpcSendResp, err := BuildAndSubmitTransaction(ctx, "ErrorHandlerServiceNonJitterChannel", payload, p.Store, p.TxService) + rpcSendResp, err := p.TxManager.BuildAndSubmitTransaction(ctx, ErrorNonJitterChannelName, payload) if err != nil { - log.Errorf(err.Error()) + log.Errorf("%s: Unable to sign and submit transaction: %e", ErrorNonJitterChannelName, err) return } payload.RpcSubmitTxResponse = rpcSendResp if !slices.Contains(tss.NonJitterErrorCodes, rpcSendResp.Code.TxResultCode) { p.Router.Route(payload) + if err != nil { + log.Errorf("%s: Unable to route payload: %e", ErrorNonJitterChannelName, err) + return + } return } } @@ -72,10 +79,10 @@ func (p *rpcErrorHandlerServiceNonJitterPool) Receive(payload tss.Payload) { } } -func (p *rpcErrorHandlerServiceNonJitterPool) SetRouter(router router.Router) { +func (p *errorNonJitterPool) SetRouter(router router.Router) { p.Router = router } -func (p *rpcErrorHandlerServiceNonJitterPool) Stop() { +func (p *errorNonJitterPool) Stop() { p.Pool.StopAndWait() } diff --git a/internal/tss/channels/error_non_jitter_channel_test.go b/internal/tss/channels/error_non_jitter_channel_test.go new file mode 100644 index 0000000..94fef17 --- /dev/null +++ b/internal/tss/channels/error_non_jitter_channel_test.go @@ -0,0 +1,140 @@ +package channels + +import ( + "context" + "errors" + "testing" + + "github.com/stellar/wallet-backend/internal/db" + "github.com/stellar/wallet-backend/internal/db/dbtest" + "github.com/stellar/wallet-backend/internal/entities" + "github.com/stellar/wallet-backend/internal/tss" + "github.com/stellar/wallet-backend/internal/tss/router" + "github.com/stellar/wallet-backend/internal/tss/services" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestNonJitterSend(t *testing.T) { + dbt := dbtest.Open(t) + defer dbt.Close() + dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) + require.NoError(t, err) + defer dbConnectionPool.Close() + txManagerMock := services.TransactionManagerMock{} + routerMock := router.MockRouter{} + cfg := ErrorNonJitterChannelConfigs{ + TxManager: &txManagerMock, + Router: &routerMock, + MaxBufferSize: 1, + MaxWorkers: 1, + MaxRetries: 3, + WaitBtwnRetriesMS: 10, + } + + channel := NewErrorNonJitterChannel(cfg) + + payload := tss.Payload{} + payload.WebhookURL = "www.stellar.com" + payload.TransactionHash = "hash" + payload.TransactionXDR = "xdr" + + rpcResp := tss.RPCSendTxResponse{ + Status: tss.RPCTXStatus{RPCStatus: entities.ErrorStatus}, + Code: tss.RPCTXCode{TxResultCode: tss.JitterErrorCodes[0]}, + } + payload.RpcSubmitTxResponse = rpcResp + + txManagerMock. + On("BuildAndSubmitTransaction", context.Background(), ErrorNonJitterChannelName, payload). + Return(rpcResp, nil). + Once() + + routerMock. + On("Route", payload). + Return(nil). + Once() + + channel.Send(payload) + channel.Stop() + + routerMock.AssertCalled(t, "Route", payload) +} + +func TestNonJitterReceive(t *testing.T) { + dbt := dbtest.Open(t) + defer dbt.Close() + dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) + require.NoError(t, err) + defer dbConnectionPool.Close() + txManagerMock := services.TransactionManagerMock{} + routerMock := router.MockRouter{} + cfg := ErrorNonJitterChannelConfigs{ + TxManager: &txManagerMock, + Router: &routerMock, + MaxBufferSize: 1, + MaxWorkers: 1, + MaxRetries: 3, + WaitBtwnRetriesMS: 10, + } + + channel := NewErrorNonJitterChannel(cfg) + payload := tss.Payload{} + payload.WebhookURL = "www.stellar.com" + payload.TransactionHash = "hash" + payload.TransactionXDR = "xdr" + t.Run("build_and_submit_tx_fail", func(t *testing.T) { + txManagerMock. + On("BuildAndSubmitTransaction", context.Background(), ErrorNonJitterChannelName, payload). + Return(tss.RPCSendTxResponse{}, errors.New("build tx failed")). + Once() + + channel.Receive(payload) + + routerMock.AssertNotCalled(t, "Route", payload) + }) + + t.Run("retries", func(t *testing.T) { + sendResp1 := tss.RPCSendTxResponse{ + Status: tss.RPCTXStatus{RPCStatus: entities.ErrorStatus}, + Code: tss.RPCTXCode{TxResultCode: tss.NonJitterErrorCodes[0]}, + } + sendResp2 := tss.RPCSendTxResponse{ + Status: tss.RPCTXStatus{RPCStatus: entities.ErrorStatus}, + Code: tss.RPCTXCode{TxResultCode: tss.JitterErrorCodes[0]}, + } + + txManagerMock. + On("BuildAndSubmitTransaction", context.Background(), ErrorNonJitterChannelName, mock.AnythingOfType("tss.Payload")). + Return(sendResp1, nil). + Once() + txManagerMock. + On("BuildAndSubmitTransaction", context.Background(), ErrorNonJitterChannelName, mock.AnythingOfType("tss.Payload")). + Return(sendResp2, nil). + Once() + routerMock. + On("Route", mock.AnythingOfType("tss.Payload")). + Return(nil). + Once() + + channel.Receive(payload) + }) + + t.Run("max_retries", func(t *testing.T) { + sendResp := tss.RPCSendTxResponse{ + Status: tss.RPCTXStatus{RPCStatus: entities.ErrorStatus}, + Code: tss.RPCTXCode{TxResultCode: tss.NonJitterErrorCodes[0]}, + } + + txManagerMock. + On("BuildAndSubmitTransaction", context.Background(), ErrorNonJitterChannelName, mock.AnythingOfType("tss.Payload")). + Return(sendResp, nil). + Times(3) + routerMock. + On("Route", mock.AnythingOfType("tss.Payload")). + Return(nil). + Once() + + channel.Receive(payload) + }) +} diff --git a/internal/tss/channels/error_service_handler_jitter_channel_test.go b/internal/tss/channels/error_service_handler_jitter_channel_test.go deleted file mode 100644 index bb4077d..0000000 --- a/internal/tss/channels/error_service_handler_jitter_channel_test.go +++ /dev/null @@ -1,224 +0,0 @@ -package channels - -import ( - "context" - "errors" - "testing" - - "github.com/stellar/go/xdr" - "github.com/stellar/wallet-backend/internal/db" - "github.com/stellar/wallet-backend/internal/db/dbtest" - "github.com/stellar/wallet-backend/internal/tss" - "github.com/stellar/wallet-backend/internal/tss/router" - "github.com/stellar/wallet-backend/internal/tss/store" - "github.com/stellar/wallet-backend/internal/tss/utils" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" -) - -func TestJitterSend(t *testing.T) { - dbt := dbtest.Open(t) - defer dbt.Close() - - dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) - require.NoError(t, err) - defer dbConnectionPool.Close() - store := store.NewStore(dbConnectionPool) - txServiceMock := utils.TransactionServiceMock{} - cfg := RPCErrorHandlerServiceJitterChannelConfigs{ - Store: store, - TxService: &txServiceMock, - MaxBufferSize: 1, - MaxWorkers: 1, - MaxRetries: 3, - MinWaitBtwnRetriesMS: 10, - } - channel := NewErrorHandlerServiceJitterChannel(cfg) - - payload := tss.Payload{} - payload.WebhookURL = "www.stellar.com" - payload.TransactionHash = "hash" - payload.TransactionXDR = "xdr" - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(nil, errors.New("signing failed")) - - _ = store.UpsertTransaction(context.Background(), payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.NewStatus) - - channel.Send(payload) - channel.Stop() - - var status string - err = dbConnectionPool.GetContext(context.Background(), &status, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, status, string(tss.NewStatus)) -} - -func TestJitterReceive(t *testing.T) { - dbt := dbtest.Open(t) - defer dbt.Close() - - dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) - require.NoError(t, err) - defer dbConnectionPool.Close() - store := store.NewStore(dbConnectionPool) - txServiceMock := utils.TransactionServiceMock{} - cfg := RPCErrorHandlerServiceJitterChannelConfigs{ - Store: store, - TxService: &txServiceMock, - MaxBufferSize: 1, - MaxWorkers: 1, - MaxRetries: 3, - MinWaitBtwnRetriesMS: 10, - } - channel := NewErrorHandlerServiceJitterChannel(cfg) - - mockRouter := router.MockRouter{} - defer mockRouter.AssertExpectations(t) - channel.SetRouter(&mockRouter) - networkPass := "passphrase" - feeBumpTx := utils.BuildTestFeeBumpTransaction() - feeBumpTxXDR, _ := feeBumpTx.Base64() - feeBumpTxHash, _ := feeBumpTx.HashHex(networkPass) - payload := tss.Payload{} - payload.WebhookURL = "www.stellar.com" - payload.TransactionHash = "hash" - payload.TransactionXDR = "xdr" - - _ = store.UpsertTransaction(context.Background(), payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.NewStatus) - - t.Run("signing_and_submitting_tx_fails", func(t *testing.T) { - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(nil, errors.New("sign tx failed")). - Once() - - channel.Receive(payload) - - var txStatus string - err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, string(tss.NewStatus), txStatus) - }) - - t.Run("payload_gets_routed", func(t *testing.T) { - sendResp := tss.RPCSendTxResponse{} - sendResp.Status = tss.ErrorStatus - sendResp.TransactionHash = feeBumpTxHash - sendResp.TransactionXDR = feeBumpTxXDR - sendResp.Code.TxResultCode = xdr.TransactionResultCodeTxTooEarly - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(feeBumpTx, nil). - Once(). - On("NetworkPassphrase"). - Return(networkPass). - Once(). - On("SendTransaction", feeBumpTxXDR). - Return(sendResp, nil). - Once() - - mockRouter. - On("Route", mock.AnythingOfType("tss.Payload")). - Return(). - Once() - - channel.Receive(payload) - - var txStatus string - err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, string(tss.ErrorStatus), txStatus) - - var tryStatus int - err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) - require.NoError(t, err) - assert.Equal(t, int(xdr.TransactionResultCodeTxTooEarly), tryStatus) - }) - - t.Run("retries", func(t *testing.T) { - sendResp1 := tss.RPCSendTxResponse{} - sendResp1.Status = tss.ErrorStatus - sendResp1.TransactionHash = feeBumpTxHash - sendResp1.TransactionXDR = feeBumpTxXDR - sendResp1.Code.TxResultCode = xdr.TransactionResultCodeTxInsufficientFee - - sendResp2 := tss.RPCSendTxResponse{} - sendResp2.Status = tss.FailedStatus - sendResp2.TransactionHash = feeBumpTxHash - sendResp2.TransactionXDR = feeBumpTxXDR - sendResp2.Code.TxResultCode = xdr.TransactionResultCodeTxFailed - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(feeBumpTx, nil). - Twice(). - On("NetworkPassphrase"). - Return(networkPass). - Twice() - - txServiceMock. - On("SendTransaction", feeBumpTxXDR). - Return(sendResp1, nil). - Once() - - txServiceMock. - On("SendTransaction", feeBumpTxXDR). - Return(sendResp2, nil). - Once() - - mockRouter. - On("Route", mock.AnythingOfType("tss.Payload")). - Return(). - Once() - - channel.Receive(payload) - - var txStatus string - err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, string(tss.FailedStatus), txStatus) - - var tryStatus int - err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) - require.NoError(t, err) - assert.Equal(t, int(xdr.TransactionResultCodeTxFailed), tryStatus) - }) - - t.Run("max_retries", func(t *testing.T) { - sendResp := tss.RPCSendTxResponse{} - sendResp.Status = tss.ErrorStatus - sendResp.TransactionHash = feeBumpTxHash - sendResp.TransactionXDR = feeBumpTxXDR - sendResp.Code.TxResultCode = xdr.TransactionResultCodeTxInsufficientFee - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(feeBumpTx, nil). - Times(3). - On("NetworkPassphrase"). - Return(networkPass). - Times(3) - - txServiceMock. - On("SendTransaction", feeBumpTxXDR). - Return(sendResp, nil). - Times(3) - - mockRouter. - On("Route", mock.AnythingOfType("tss.Payload")). - Return(). - Once() - - channel.Receive(payload) - - var txStatus string - err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, string(tss.ErrorStatus), txStatus) - - var tryStatus int - err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) - require.NoError(t, err) - assert.Equal(t, int(xdr.TransactionResultCodeTxInsufficientFee), tryStatus) - }) -} diff --git a/internal/tss/channels/rpc_caller_channel.go b/internal/tss/channels/rpc_caller_channel.go new file mode 100644 index 0000000..9a36318 --- /dev/null +++ b/internal/tss/channels/rpc_caller_channel.go @@ -0,0 +1,81 @@ +package channels + +import ( + "context" + + "github.com/alitto/pond" + + "github.com/stellar/go/support/log" + "github.com/stellar/wallet-backend/internal/entities" + "github.com/stellar/wallet-backend/internal/tss" + "github.com/stellar/wallet-backend/internal/tss/router" + "github.com/stellar/wallet-backend/internal/tss/services" + "github.com/stellar/wallet-backend/internal/tss/store" +) + +type RPCCallerChannelConfigs struct { + TxManager services.TransactionManager + Router router.Router + Store store.Store + MaxBufferSize int + MaxWorkers int +} + +type rpcCallerPool struct { + Pool *pond.WorkerPool + TxManager services.TransactionManager + Router router.Router + Store store.Store +} + +var RPCCallerChannelName = "RPCCallerChannel" + +func NewRPCCallerChannel(cfg RPCCallerChannelConfigs) *rpcCallerPool { + pool := pond.New(cfg.MaxBufferSize, cfg.MaxWorkers, pond.Strategy(pond.Balanced())) + return &rpcCallerPool{ + Pool: pool, + TxManager: cfg.TxManager, + Store: cfg.Store, + Router: cfg.Router, + } + +} + +func (p *rpcCallerPool) Send(payload tss.Payload) { + p.Pool.Submit(func() { + p.Receive(payload) + }) +} + +func (p *rpcCallerPool) Receive(payload tss.Payload) { + + ctx := context.Background() + // Create a new transaction record in the transactions table. + err := p.Store.UpsertTransaction(ctx, payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.RPCTXStatus{OtherStatus: tss.NewStatus}) + + if err != nil { + log.Errorf("%s: Unable to upsert transaction into transactions table: %e", RPCCallerChannelName, err) + return + } + rpcSendResp, err := p.TxManager.BuildAndSubmitTransaction(ctx, RPCCallerChannelName, payload) + + if err != nil { + log.Errorf("%s: Unable to sign and submit transaction: %e", RPCCallerChannelName, err) + return + } + payload.RpcSubmitTxResponse = rpcSendResp + if rpcSendResp.Status.RPCStatus == entities.TryAgainLaterStatus || rpcSendResp.Status.RPCStatus == entities.ErrorStatus { + err = p.Router.Route(payload) + if err != nil { + log.Errorf("%s: Unable to route payload: %e", RPCCallerChannelName, err) + } + } +} + +func (p *rpcCallerPool) SetRouter(router router.Router) { + p.Router = router +} + +func (p *rpcCallerPool) Stop() { + p.Pool.StopAndWait() +} diff --git a/internal/tss/channels/rpc_caller_channel_test.go b/internal/tss/channels/rpc_caller_channel_test.go new file mode 100644 index 0000000..b5899c0 --- /dev/null +++ b/internal/tss/channels/rpc_caller_channel_test.go @@ -0,0 +1,130 @@ +package channels + +import ( + "context" + "errors" + "testing" + + "github.com/stellar/wallet-backend/internal/db" + "github.com/stellar/wallet-backend/internal/db/dbtest" + "github.com/stellar/wallet-backend/internal/entities" + "github.com/stellar/wallet-backend/internal/tss" + "github.com/stellar/wallet-backend/internal/tss/router" + "github.com/stellar/wallet-backend/internal/tss/services" + "github.com/stellar/wallet-backend/internal/tss/store" + "github.com/stretchr/testify/require" +) + +func TestSend(t *testing.T) { + dbt := dbtest.Open(t) + defer dbt.Close() + dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) + require.NoError(t, err) + defer dbConnectionPool.Close() + store := store.NewStore(dbConnectionPool) + txManagerMock := services.TransactionManagerMock{} + routerMock := router.MockRouter{} + cfgs := RPCCallerChannelConfigs{ + Store: store, + TxManager: &txManagerMock, + Router: &routerMock, + MaxBufferSize: 10, + MaxWorkers: 10, + } + channel := NewRPCCallerChannel(cfgs) + payload := tss.Payload{} + payload.WebhookURL = "www.stellar.com" + payload.TransactionHash = "hash" + payload.TransactionXDR = "xdr" + + rpcResp := tss.RPCSendTxResponse{ + Status: tss.RPCTXStatus{RPCStatus: entities.TryAgainLaterStatus}, + } + payload.RpcSubmitTxResponse = rpcResp + + txManagerMock. + On("BuildAndSubmitTransaction", context.Background(), RPCCallerChannelName, payload). + Return(rpcResp, nil). + Once() + + routerMock. + On("Route", payload). + Return(nil). + Once() + + channel.Send(payload) + channel.Stop() + + routerMock.AssertCalled(t, "Route", payload) +} + +func TestReceivee(t *testing.T) { + dbt := dbtest.Open(t) + defer dbt.Close() + dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) + require.NoError(t, err) + defer dbConnectionPool.Close() + store := store.NewStore(dbConnectionPool) + txManagerMock := services.TransactionManagerMock{} + routerMock := router.MockRouter{} + cfgs := RPCCallerChannelConfigs{ + Store: store, + TxManager: &txManagerMock, + Router: &routerMock, + MaxBufferSize: 10, + MaxWorkers: 10, + } + channel := NewRPCCallerChannel(cfgs) + payload := tss.Payload{} + payload.WebhookURL = "www.stellar.com" + payload.TransactionHash = "hash" + payload.TransactionXDR = "xdr" + + t.Run("build_and_submit_tx_fail", func(t *testing.T) { + txManagerMock. + On("BuildAndSubmitTransaction", context.Background(), RPCCallerChannelName, payload). + Return(tss.RPCSendTxResponse{}, errors.New("build tx failed")). + Once() + + channel.Receive(payload) + + routerMock.AssertNotCalled(t, "Route", payload) + }) + + t.Run("payload_not_routed", func(t *testing.T) { + rpcResp := tss.RPCSendTxResponse{ + Status: tss.RPCTXStatus{RPCStatus: entities.PendingStatus}, + } + payload.RpcSubmitTxResponse = rpcResp + + txManagerMock. + On("BuildAndSubmitTransaction", context.Background(), RPCCallerChannelName, payload). + Return(rpcResp, nil). + Once() + + channel.Receive(payload) + + routerMock.AssertNotCalled(t, "Route", payload) + }) + t.Run("payload_routed", func(t *testing.T) { + rpcResp := tss.RPCSendTxResponse{ + Status: tss.RPCTXStatus{RPCStatus: entities.ErrorStatus}, + } + payload.RpcSubmitTxResponse = rpcResp + + txManagerMock. + On("BuildAndSubmitTransaction", context.Background(), RPCCallerChannelName, payload). + Return(rpcResp, nil). + Once() + + routerMock. + On("Route", payload). + Return(nil). + Once() + + channel.Receive(payload) + + routerMock.AssertCalled(t, "Route", payload) + }) + +} diff --git a/internal/tss/channels/rpc_caller_service_channel.go b/internal/tss/channels/rpc_caller_service_channel.go deleted file mode 100644 index 33300c3..0000000 --- a/internal/tss/channels/rpc_caller_service_channel.go +++ /dev/null @@ -1,78 +0,0 @@ -package channels - -import ( - "context" - - "github.com/alitto/pond" - - "github.com/stellar/go/support/log" - "github.com/stellar/wallet-backend/internal/tss" - "github.com/stellar/wallet-backend/internal/tss/router" - "github.com/stellar/wallet-backend/internal/tss/services" - "github.com/stellar/wallet-backend/internal/tss/store" - "github.com/stellar/wallet-backend/internal/tss/utils" -) - -type RPCCallerServiceChannelConfigs struct { - Store store.Store - TxService utils.TransactionService - Router router.Router - MaxBufferSize int - MaxWorkers int -} - -type rpcCallerServicePool struct { - Pool *pond.WorkerPool - TxService utils.TransactionService - ErrHandlerService services.Service - Store store.Store - Router router.Router -} - -func NewRPCCallerServiceChannel(cfg RPCCallerServiceChannelConfigs) *rpcCallerServicePool { - pool := pond.New(cfg.MaxBufferSize, cfg.MaxWorkers, pond.Strategy(pond.Balanced())) - return &rpcCallerServicePool{ - Pool: pool, - TxService: cfg.TxService, - Store: cfg.Store, - Router: cfg.Router, - } - -} - -func (p *rpcCallerServicePool) Send(payload tss.Payload) { - p.Pool.Submit(func() { - p.Receive(payload) - }) -} - -func (p *rpcCallerServicePool) Receive(payload tss.Payload) { - - ctx := context.Background() - // Create a new transaction record in the transactions table. - err := p.Store.UpsertTransaction(ctx, payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.NewStatus) - - if err != nil { - log.Errorf("Unable to upsert transaction into transactions table: %s", err.Error()) - return - } - - rpcSendResp, err := BuildAndSubmitTransaction(ctx, "RPCCallerServiceChannel", payload, p.Store, p.TxService) - - if err != nil { - log.Errorf(": Unable to sign and submit transaction: %s", err.Error()) - return - } - payload.RpcSubmitTxResponse = rpcSendResp - if rpcSendResp.Status == tss.TryAgainLaterStatus || rpcSendResp.Status == tss.ErrorStatus { - p.Router.Route(payload) - } -} - -func (p *rpcCallerServicePool) SetRouter(router router.Router) { - p.Router = router -} - -func (p *rpcCallerServicePool) Stop() { - p.Pool.StopAndWait() -} diff --git a/internal/tss/channels/rpc_caller_service_channel_test.go b/internal/tss/channels/rpc_caller_service_channel_test.go deleted file mode 100644 index 797b56d..0000000 --- a/internal/tss/channels/rpc_caller_service_channel_test.go +++ /dev/null @@ -1,207 +0,0 @@ -package channels - -import ( - "context" - "errors" - "testing" - - "github.com/stellar/go/xdr" - "github.com/stellar/wallet-backend/internal/db" - "github.com/stellar/wallet-backend/internal/db/dbtest" - "github.com/stellar/wallet-backend/internal/tss" - "github.com/stellar/wallet-backend/internal/tss/router" - "github.com/stellar/wallet-backend/internal/tss/store" - "github.com/stellar/wallet-backend/internal/tss/utils" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" -) - -func TestSend(t *testing.T) { - dbt := dbtest.Open(t) - defer dbt.Close() - dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) - require.NoError(t, err) - defer dbConnectionPool.Close() - store := store.NewStore(dbConnectionPool) - txServiceMock := utils.TransactionServiceMock{} - cfgs := RPCCallerServiceChannelConfigs{ - Store: store, - TxService: &txServiceMock, - MaxBufferSize: 10, - MaxWorkers: 10, - } - channel := NewRPCCallerServiceChannel(cfgs) - payload := tss.Payload{} - payload.WebhookURL = "www.stellar.com" - payload.TransactionHash = "hash" - payload.TransactionXDR = "xdr" - networkPass := "passphrase" - - feeBumpTx := utils.BuildTestFeeBumpTransaction() - feeBumpTxXDR, _ := feeBumpTx.Base64() - sendResp := tss.RPCSendTxResponse{} - sendResp.Code.OtherCodes = tss.RPCFailCode - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(feeBumpTx, nil). - Once(). - On("NetworkPassphrase"). - Return(networkPass). - Once(). - On("SendTransaction", feeBumpTxXDR). - Return(sendResp, errors.New("RPC Fail")). - Once() - - channel.Send(payload) - channel.Stop() - - var status string - err = dbConnectionPool.GetContext(context.Background(), &status, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, status, string(tss.NewStatus)) - - var tryStatus int - feeBumpTxHash, _ := feeBumpTx.HashHex(networkPass) - err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) - require.NoError(t, err) - assert.Equal(t, int(tss.RPCFailCode), tryStatus) -} - -func TestReceive(t *testing.T) { - dbt := dbtest.Open(t) - defer dbt.Close() - - dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) - require.NoError(t, err) - defer dbConnectionPool.Close() - store := store.NewStore(dbConnectionPool) - txServiceMock := utils.TransactionServiceMock{} - defer txServiceMock.AssertExpectations(t) - routerMock := router.MockRouter{} - defer routerMock.AssertExpectations(t) - cfgs := RPCCallerServiceChannelConfigs{ - Store: store, - TxService: &txServiceMock, - Router: &routerMock, - MaxBufferSize: 1, - MaxWorkers: 1, - } - networkPass := "passphrase" - channel := NewRPCCallerServiceChannel(cfgs) - feeBumpTx := utils.BuildTestFeeBumpTransaction() - feeBumpTxXDR, _ := feeBumpTx.Base64() - feeBumpTxHash, _ := feeBumpTx.HashHex(networkPass) - payload := tss.Payload{} - payload.WebhookURL = "www.stellar.com" - payload.TransactionHash = "hash" - payload.TransactionXDR = "xdr" - - t.Run("fail_on_tx_build_and_sign", func(t *testing.T) { - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(nil, errors.New("signing failed")). - Once() - channel.Receive(payload) - - var status string - err = dbConnectionPool.GetContext(context.Background(), &status, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, string(tss.NewStatus), status) - }) - - t.Run("sign_and_submit_tx_fails", func(t *testing.T) { - sendResp := tss.RPCSendTxResponse{} - sendResp.Code.OtherCodes = tss.RPCFailCode - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(feeBumpTx, nil). - Once(). - On("NetworkPassphrase"). - Return(networkPass). - Once(). - On("SendTransaction", feeBumpTxXDR). - Return(sendResp, errors.New("RPC Fail")). - Once() - - channel.Receive(payload) - - var txStatus string - err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, txStatus, string(tss.NewStatus)) - - var tryStatus int - feeBumpTxHash, _ := feeBumpTx.HashHex(networkPass) - err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) - require.NoError(t, err) - assert.Equal(t, int(tss.RPCFailCode), tryStatus) - - }) - - t.Run("routes_payload", func(t *testing.T) { - sendResp := tss.RPCSendTxResponse{} - sendResp.Status = tss.ErrorStatus - sendResp.TransactionHash = feeBumpTxHash - sendResp.TransactionXDR = feeBumpTxXDR - sendResp.Code.TxResultCode = xdr.TransactionResultCodeTxTooEarly - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(feeBumpTx, nil). - Once(). - On("NetworkPassphrase"). - Return(networkPass). - Once(). - On("SendTransaction", feeBumpTxXDR). - Return(sendResp, nil). - Once() - routerMock. - On("Route", mock.AnythingOfType("tss.Payload")). - Return(). - Once() - - channel.Receive(payload) - - var txStatus string - err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, string(tss.ErrorStatus), txStatus) - - var tryStatus int - err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) - require.NoError(t, err) - assert.Equal(t, int(xdr.TransactionResultCodeTxTooEarly), tryStatus) - }) - - t.Run("does_not_routes_payload", func(t *testing.T) { - sendResp := tss.RPCSendTxResponse{} - sendResp.Status = tss.PendingStatus - sendResp.TransactionHash = feeBumpTxHash - sendResp.TransactionXDR = feeBumpTxXDR - sendResp.Code.TxResultCode = xdr.TransactionResultCodeTxSuccess - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(feeBumpTx, nil). - Once(). - On("NetworkPassphrase"). - Return(networkPass). - Once(). - On("SendTransaction", feeBumpTxXDR). - Return(sendResp, nil). - Once() - // this time the router mock is not called - - channel.Receive(payload) - - var txStatus string - err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, string(tss.PendingStatus), txStatus) - - var tryStatus int - err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) - require.NoError(t, err) - assert.Equal(t, int(xdr.TransactionResultCodeTxSuccess), tryStatus) - }) - -} diff --git a/internal/tss/channels/utils.go b/internal/tss/channels/utils.go deleted file mode 100644 index a5c59cc..0000000 --- a/internal/tss/channels/utils.go +++ /dev/null @@ -1,46 +0,0 @@ -package channels - -import ( - "fmt" - - "github.com/stellar/wallet-backend/internal/tss" - "github.com/stellar/wallet-backend/internal/tss/store" - "github.com/stellar/wallet-backend/internal/tss/utils" - "golang.org/x/net/context" -) - -func BuildAndSubmitTransaction(ctx context.Context, channelName string, payload tss.Payload, store store.Store, txService utils.TransactionService) (tss.RPCSendTxResponse, error) { - feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(ctx, payload.TransactionXDR) - if err != nil { - return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to sign/build transaction: %s", channelName, err.Error()) - } - feeBumpTxHash, err := feeBumpTx.HashHex(txService.NetworkPassphrase()) - if err != nil { - return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to hashhex fee bump transaction: %s", channelName, err.Error()) - } - - feeBumpTxXDR, err := feeBumpTx.Base64() - if err != nil { - return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to base64 fee bump transaction: %s", channelName, err.Error()) - } - - err = store.UpsertTry(ctx, payload.TransactionHash, feeBumpTxHash, feeBumpTxXDR, tss.RPCTXCode{OtherCodes: tss.NewCode}) - if err != nil { - return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to upsert try in tries table: %s", channelName, err.Error()) - } - rpcSendResp, rpcErr := txService.SendTransaction(feeBumpTxXDR) - - err = store.UpsertTry(ctx, payload.TransactionHash, feeBumpTxHash, feeBumpTxXDR, rpcSendResp.Code) - if err != nil { - return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to upsert try in tries table: %s", channelName, err.Error()) - } - if rpcErr != nil && rpcSendResp.Code.OtherCodes == tss.RPCFailCode || rpcSendResp.Code.OtherCodes == tss.UnMarshalBinaryCode { - return tss.RPCSendTxResponse{}, fmt.Errorf("%s: RPC fail: %s", channelName, rpcErr.Error()) - } - - err = store.UpsertTransaction(ctx, payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, rpcSendResp.Status) - if err != nil { - return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to do the final update of tx in the transactions table: %s", channelName, err.Error()) - } - return rpcSendResp, nil -} diff --git a/internal/tss/channels/utils_test.go b/internal/tss/channels/utils_test.go deleted file mode 100644 index c3dcb97..0000000 --- a/internal/tss/channels/utils_test.go +++ /dev/null @@ -1,143 +0,0 @@ -package channels - -import ( - "context" - "errors" - "testing" - - "github.com/stellar/go/xdr" - "github.com/stellar/wallet-backend/internal/db" - "github.com/stellar/wallet-backend/internal/db/dbtest" - "github.com/stellar/wallet-backend/internal/tss" - "github.com/stellar/wallet-backend/internal/tss/store" - "github.com/stellar/wallet-backend/internal/tss/utils" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestBuildAndSubmitTransaction(t *testing.T) { - dbt := dbtest.Open(t) - defer dbt.Close() - - dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) - require.NoError(t, err) - defer dbConnectionPool.Close() - store := store.NewStore(dbConnectionPool) - txServiceMock := utils.TransactionServiceMock{} - networkPass := "passphrase" - feeBumpTx := utils.BuildTestFeeBumpTransaction() - feeBumpTxXDR, _ := feeBumpTx.Base64() - feeBumpTxHash, _ := feeBumpTx.HashHex(networkPass) - payload := tss.Payload{} - payload.WebhookURL = "www.stellar.com" - payload.TransactionHash = "hash" - payload.TransactionXDR = "xdr" - - _ = store.UpsertTransaction(context.Background(), payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.NewStatus) - t.Run("fail_on_tx_build_and_sign", func(t *testing.T) { - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(nil, errors.New("signing failed")). - Once() - - _, err := BuildAndSubmitTransaction(context.Background(), "channel", payload, store, &txServiceMock) - - assert.Equal(t, "channel: Unable to sign/build transaction: signing failed", err.Error()) - - var status string - err = dbConnectionPool.GetContext(context.Background(), &status, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, string(tss.NewStatus), status) - }) - - t.Run("rpc_call_fail", func(t *testing.T) { - sendResp := tss.RPCSendTxResponse{} - sendResp.Code.OtherCodes = tss.RPCFailCode - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(feeBumpTx, nil). - Once(). - On("NetworkPassphrase"). - Return(networkPass). - Once(). - On("SendTransaction", feeBumpTxXDR). - Return(sendResp, errors.New("RPC Fail")). - Once() - - _, err := BuildAndSubmitTransaction(context.Background(), "channel", payload, store, &txServiceMock) - - assert.Equal(t, "channel: RPC fail: RPC Fail", err.Error()) - - var txStatus string - err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, txStatus, string(tss.NewStatus)) - - var tryStatus int - err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) - require.NoError(t, err) - assert.Equal(t, int(tss.RPCFailCode), tryStatus) - }) - - t.Run("rpc_resp_unmarshaling_error", func(t *testing.T) { - sendResp := tss.RPCSendTxResponse{} - sendResp.Code.OtherCodes = tss.UnMarshalBinaryCode - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(feeBumpTx, nil). - Once(). - On("NetworkPassphrase"). - Return(networkPass). - Once(). - On("SendTransaction", feeBumpTxXDR). - Return(sendResp, errors.New("unable to unmarshal")). - Once() - - _, err := BuildAndSubmitTransaction(context.Background(), "channel", payload, store, &txServiceMock) - - assert.Equal(t, "channel: RPC fail: unable to unmarshal", err.Error()) - - var txStatus string - err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, txStatus, string(tss.NewStatus)) - - var tryStatus int - err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) - require.NoError(t, err) - assert.Equal(t, int(tss.UnMarshalBinaryCode), tryStatus) - }) - t.Run("rpc_returns_response", func(t *testing.T) { - sendResp := tss.RPCSendTxResponse{} - sendResp.Status = tss.TryAgainLaterStatus - sendResp.TransactionHash = feeBumpTxHash - sendResp.TransactionXDR = feeBumpTxXDR - sendResp.Code.TxResultCode = xdr.TransactionResultCodeTxInsufficientFee - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(feeBumpTx, nil). - Once(). - On("NetworkPassphrase"). - Return(networkPass). - Once(). - On("SendTransaction", feeBumpTxXDR). - Return(sendResp, nil). - Once() - - resp, err := BuildAndSubmitTransaction(context.Background(), "channel", payload, store, &txServiceMock) - - assert.Equal(t, tss.TryAgainLaterStatus, resp.Status) - assert.Equal(t, xdr.TransactionResultCodeTxInsufficientFee, resp.Code.TxResultCode) - assert.Empty(t, err) - - var txStatus string - err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, string(tss.TryAgainLaterStatus), txStatus) - - var tryStatus int - err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) - require.NoError(t, err) - assert.Equal(t, int(xdr.TransactionResultCodeTxInsufficientFee), tryStatus) - }) -} diff --git a/internal/tss/router/mocks.go b/internal/tss/router/mocks.go index 3f4406c..2f269b7 100644 --- a/internal/tss/router/mocks.go +++ b/internal/tss/router/mocks.go @@ -11,6 +11,7 @@ type MockRouter struct { var _ Router = (*MockRouter)(nil) -func (r *MockRouter) Route(payload tss.Payload) { - r.Called(payload) +func (r *MockRouter) Route(payload tss.Payload) error { + args := r.Called(payload) + return args.Error(0) } diff --git a/internal/tss/router/router.go b/internal/tss/router/router.go index e0d3945..8c6801a 100644 --- a/internal/tss/router/router.go +++ b/internal/tss/router/router.go @@ -1,65 +1,68 @@ package router import ( + "fmt" "slices" - "github.com/stellar/go/xdr" + "github.com/stellar/wallet-backend/internal/entities" "github.com/stellar/wallet-backend/internal/tss" - "github.com/stellar/wallet-backend/internal/tss/services" ) type Router interface { - Route(payload tss.Payload) + Route(payload tss.Payload) error } type RouterConfigs struct { - ErrorHandlerService services.Service - WebhookHandlerService services.Service + RPCCallerChannel tss.Channel + ErrorJitterChannel tss.Channel + ErrorNonJitterChannel tss.Channel + WebhookChannel tss.Channel } type router struct { - ErrorHandlerService services.Service - WebhookHandlerService services.Service + RPCCallerChannel tss.Channel + ErrorJitterChannel tss.Channel + ErrorNonJitterChannel tss.Channel + WebhookChannel tss.Channel } var _ Router = (*router)(nil) -var FinalErrorCodes = []xdr.TransactionResultCode{ - xdr.TransactionResultCodeTxSuccess, - xdr.TransactionResultCodeTxFailed, - xdr.TransactionResultCodeTxMissingOperation, - xdr.TransactionResultCodeTxInsufficientBalance, - xdr.TransactionResultCodeTxBadAuthExtra, - xdr.TransactionResultCodeTxMalformed, -} - -var RetryErrorCodes = []xdr.TransactionResultCode{ - xdr.TransactionResultCodeTxTooLate, - xdr.TransactionResultCodeTxInsufficientFee, - xdr.TransactionResultCodeTxInternalError, - xdr.TransactionResultCodeTxBadSeq, -} - func NewRouter(cfg RouterConfigs) Router { return &router{ - ErrorHandlerService: cfg.ErrorHandlerService, - WebhookHandlerService: cfg.WebhookHandlerService, + RPCCallerChannel: cfg.RPCCallerChannel, + ErrorJitterChannel: cfg.ErrorJitterChannel, + ErrorNonJitterChannel: cfg.ErrorNonJitterChannel, + WebhookChannel: cfg.WebhookChannel, } } -func (r *router) Route(payload tss.Payload) { - switch payload.RpcSubmitTxResponse.Status { - case tss.TryAgainLaterStatus: - r.ErrorHandlerService.ProcessPayload(payload) - case tss.ErrorStatus: - if payload.RpcSubmitTxResponse.Code.OtherCodes == tss.NoCode { - if slices.Contains(RetryErrorCodes, payload.RpcSubmitTxResponse.Code.TxResultCode) { - r.ErrorHandlerService.ProcessPayload(payload) - } else if slices.Contains(FinalErrorCodes, payload.RpcSubmitTxResponse.Code.TxResultCode) { - r.WebhookHandlerService.ProcessPayload(payload) +func (r *router) Route(payload tss.Payload) error { + var channel tss.Channel + if payload.RpcSubmitTxResponse.Status.Status() != "" { + switch payload.RpcSubmitTxResponse.Status.Status() { + case string(tss.NewStatus): + channel = r.RPCCallerChannel + case string(entities.TryAgainLaterStatus): + channel = r.ErrorJitterChannel + case string(entities.ErrorStatus): + if payload.RpcSubmitTxResponse.Code.OtherCodes == tss.NoCode { + if slices.Contains(tss.JitterErrorCodes, payload.RpcSubmitTxResponse.Code.TxResultCode) { + channel = r.ErrorJitterChannel + } else if slices.Contains(tss.NonJitterErrorCodes, payload.RpcSubmitTxResponse.Code.TxResultCode) { + channel = r.ErrorNonJitterChannel + } else if slices.Contains(tss.FinalErrorCodes, payload.RpcSubmitTxResponse.Code.TxResultCode) { + channel = r.WebhookChannel + } } + default: + // Do nothing for PENDING / DUPLICATE statuses + return nil } - default: - return } + if channel == nil { + return fmt.Errorf("payload could not be routed - channel is nil") + } + channel.Send(payload) + return nil } diff --git a/internal/tss/router/router_test.go b/internal/tss/router/router_test.go index 8a5907a..70768d0 100644 --- a/internal/tss/router/router_test.go +++ b/internal/tss/router/router_test.go @@ -3,49 +3,128 @@ package router import ( "testing" - "github.com/stellar/go/xdr" + "github.com/stellar/wallet-backend/internal/entities" "github.com/stellar/wallet-backend/internal/tss" - "github.com/stellar/wallet-backend/internal/tss/services" + "github.com/stretchr/testify/assert" ) func TestRouter(t *testing.T) { - errorHandlerService := services.MockService{} - defer errorHandlerService.AssertExpectations(t) - webhookHandlerService := services.MockService{} - router := NewRouter(RouterConfigs{ErrorHandlerService: &errorHandlerService, WebhookHandlerService: &webhookHandlerService}) - t.Run("status_try_again_later", func(t *testing.T) { + rpcCallerChannel := tss.MockChannel{} + defer rpcCallerChannel.AssertExpectations(t) + errorJitterChannel := tss.MockChannel{} + defer errorJitterChannel.AssertExpectations(t) + errorNonJitterChannel := tss.MockChannel{} + defer errorNonJitterChannel.AssertExpectations(t) + webhookChannel := tss.MockChannel{} + defer webhookChannel.AssertExpectations(t) + + router := NewRouter(RouterConfigs{ + RPCCallerChannel: &rpcCallerChannel, + ErrorJitterChannel: &errorJitterChannel, + ErrorNonJitterChannel: &errorNonJitterChannel, + WebhookChannel: &webhookChannel, + }) + t.Run("status_new_routes_to_rpc_caller_channel", func(t *testing.T) { payload := tss.Payload{} - payload.RpcSubmitTxResponse.Status = tss.TryAgainLaterStatus + payload.RpcSubmitTxResponse.Status = tss.RPCTXStatus{OtherStatus: tss.NewStatus} - errorHandlerService. - On("ProcessPayload", payload). + rpcCallerChannel. + On("Send", payload). Return(). Once() - router.Route(payload) + _ = router.Route(payload) + + rpcCallerChannel.AssertCalled(t, "Send", payload) }) - t.Run("error_status_route_to_error_handler_service", func(t *testing.T) { + t.Run("status_try_again_later_routes_to_error_jitter_channel", func(t *testing.T) { payload := tss.Payload{} - payload.RpcSubmitTxResponse.Status = tss.ErrorStatus - payload.RpcSubmitTxResponse.Code.TxResultCode = xdr.TransactionResultCodeTxInsufficientFee + payload.RpcSubmitTxResponse.Status = tss.RPCTXStatus{RPCStatus: entities.TryAgainLaterStatus} - errorHandlerService. - On("ProcessPayload", payload). + errorJitterChannel. + On("Send", payload). Return(). Once() - router.Route(payload) + _ = router.Route(payload) + + errorJitterChannel.AssertCalled(t, "Send", payload) }) - t.Run("error_status_route_to_webhook_handler_service", func(t *testing.T) { + t.Run("status_error_routes_to_error_jitter_channel", func(t *testing.T) { + for _, code := range tss.JitterErrorCodes { + payload := tss.Payload{ + RpcSubmitTxResponse: tss.RPCSendTxResponse{ + Status: tss.RPCTXStatus{ + RPCStatus: entities.ErrorStatus, + }, + Code: tss.RPCTXCode{ + TxResultCode: code, + }, + }, + } + payload.RpcSubmitTxResponse.Code.TxResultCode = code + errorJitterChannel. + On("Send", payload). + Return(). + Once() + + _ = router.Route(payload) + + errorJitterChannel.AssertCalled(t, "Send", payload) + } + }) + t.Run("status_error_routes_to_error_non_jitter_channel", func(t *testing.T) { + for _, code := range tss.NonJitterErrorCodes { + payload := tss.Payload{ + RpcSubmitTxResponse: tss.RPCSendTxResponse{ + Status: tss.RPCTXStatus{ + RPCStatus: entities.ErrorStatus, + }, + Code: tss.RPCTXCode{ + TxResultCode: code, + }, + }, + } + payload.RpcSubmitTxResponse.Code.TxResultCode = code + errorNonJitterChannel. + On("Send", payload). + Return(). + Once() + + _ = router.Route(payload) + + errorNonJitterChannel.AssertCalled(t, "Send", payload) + } + }) + t.Run("status_error_routes_to_webhook_channel", func(t *testing.T) { + for _, code := range tss.FinalErrorCodes { + payload := tss.Payload{ + RpcSubmitTxResponse: tss.RPCSendTxResponse{ + Status: tss.RPCTXStatus{ + RPCStatus: entities.ErrorStatus, + }, + Code: tss.RPCTXCode{ + TxResultCode: code, + }, + }, + } + payload.RpcSubmitTxResponse.Code.TxResultCode = code + webhookChannel. + On("Send", payload). + Return(). + Once() + + _ = router.Route(payload) + + webhookChannel.AssertCalled(t, "Send", payload) + } + }) + t.Run("nil_channel_does_not_route", func(t *testing.T) { payload := tss.Payload{} - payload.RpcSubmitTxResponse.Status = tss.ErrorStatus - payload.RpcSubmitTxResponse.Code.TxResultCode = xdr.TransactionResultCodeTxInsufficientBalance - webhookHandlerService. - On("ProcessPayload", payload). - Return(). - Once() + err := router.Route(payload) - router.Route(payload) + errorJitterChannel.AssertNotCalled(t, "Send", payload) + assert.Equal(t, "payload could not be routed - channel is nil", err.Error()) }) } diff --git a/internal/tss/services/error_handler_service.go b/internal/tss/services/error_handler_service.go deleted file mode 100644 index e24c310..0000000 --- a/internal/tss/services/error_handler_service.go +++ /dev/null @@ -1,36 +0,0 @@ -package services - -import ( - "slices" - - "github.com/stellar/wallet-backend/internal/tss" -) - -type errorHandlerService struct { - JitterChannel tss.Channel - NonJitterChannel tss.Channel -} - -type ErrorHandlerServiceConfigs struct { - JitterChannel tss.Channel - NonJitterChannel tss.Channel -} - -func NewErrorHandlerService(cfg ErrorHandlerServiceConfigs) *errorHandlerService { - return &errorHandlerService{ - JitterChannel: cfg.JitterChannel, - NonJitterChannel: cfg.NonJitterChannel, - } -} - -func (p *errorHandlerService) ProcessPayload(payload tss.Payload) { - if payload.RpcSubmitTxResponse.Status == tss.TryAgainLaterStatus { - p.JitterChannel.Send(payload) - } else { - if slices.Contains(tss.NonJitterErrorCodes, payload.RpcSubmitTxResponse.Code.TxResultCode) { - p.NonJitterChannel.Send(payload) - } else if slices.Contains(tss.JitterErrorCodes, payload.RpcSubmitTxResponse.Code.TxResultCode) { - p.JitterChannel.Send(payload) - } - } -} diff --git a/internal/tss/services/error_handler_service_test.go b/internal/tss/services/error_handler_service_test.go deleted file mode 100644 index 802cc8c..0000000 --- a/internal/tss/services/error_handler_service_test.go +++ /dev/null @@ -1,54 +0,0 @@ -package services - -import ( - "testing" - - "github.com/stellar/go/xdr" - "github.com/stellar/wallet-backend/internal/tss" -) - -func TestProcessPayload(t *testing.T) { - jitterChannel := tss.MockChannel{} - defer jitterChannel.AssertExpectations(t) - nonJitterChannel := tss.MockChannel{} - defer nonJitterChannel.AssertExpectations(t) - - service := NewErrorHandlerService(ErrorHandlerServiceConfigs{JitterChannel: &jitterChannel, NonJitterChannel: &nonJitterChannel}) - - t.Run("status_try_again_later", func(t *testing.T) { - payload := tss.Payload{} - payload.RpcSubmitTxResponse.Status = tss.TryAgainLaterStatus - - jitterChannel. - On("Send", payload). - Return(). - Once() - - service.ProcessPayload(payload) - }) - t.Run("code_tx_too_early", func(t *testing.T) { - payload := tss.Payload{} - payload.RpcSubmitTxResponse.Status = tss.ErrorStatus - payload.RpcSubmitTxResponse.Code.TxResultCode = xdr.TransactionResultCodeTxTooEarly - - nonJitterChannel. - On("Send", payload). - Return(). - Once() - - service.ProcessPayload(payload) - }) - - t.Run("code_tx_insufficient_fee", func(t *testing.T) { - payload := tss.Payload{} - payload.RpcSubmitTxResponse.Status = tss.ErrorStatus - payload.RpcSubmitTxResponse.Code.TxResultCode = xdr.TransactionResultCodeTxInsufficientFee - - jitterChannel. - On("Send", payload). - Return(). - Once() - - service.ProcessPayload(payload) - }) -} diff --git a/internal/tss/services/mocks.go b/internal/tss/services/mocks.go index fff8db8..3edcb26 100644 --- a/internal/tss/services/mocks.go +++ b/internal/tss/services/mocks.go @@ -1,16 +1,51 @@ package services import ( + "context" + + "github.com/stellar/go/txnbuild" "github.com/stellar/wallet-backend/internal/tss" + "github.com/stretchr/testify/mock" ) -type MockService struct { +type TransactionServiceMock struct { + mock.Mock +} + +var _ TransactionService = (*TransactionServiceMock)(nil) + +func (t *TransactionServiceMock) NetworkPassphrase() string { + args := t.Called() + return args.String(0) +} + +func (t *TransactionServiceMock) SignAndBuildNewFeeBumpTransaction(ctx context.Context, origTxXdr string) (*txnbuild.FeeBumpTransaction, error) { + args := t.Called(ctx, origTxXdr) + if result := args.Get(0); result != nil { + return result.(*txnbuild.FeeBumpTransaction), args.Error(1) + } + return nil, args.Error(1) + +} + +func (t *TransactionServiceMock) SendTransaction(transactionXdr string) (tss.RPCSendTxResponse, error) { + args := t.Called(transactionXdr) + return args.Get(0).(tss.RPCSendTxResponse), args.Error(1) +} + +func (t *TransactionServiceMock) GetTransaction(transactionHash string) (tss.RPCGetIngestTxResponse, error) { + args := t.Called(transactionHash) + return args.Get(0).(tss.RPCGetIngestTxResponse), args.Error(1) +} + +type TransactionManagerMock struct { mock.Mock } -var _ Service = (*MockService)(nil) +var _ TransactionManager = (*TransactionManagerMock)(nil) -func (s *MockService) ProcessPayload(payload tss.Payload) { - s.Called(payload) +func (t *TransactionManagerMock) BuildAndSubmitTransaction(ctx context.Context, channelName string, payload tss.Payload) (tss.RPCSendTxResponse, error) { + args := t.Called(ctx, channelName, payload) + return args.Get(0).(tss.RPCSendTxResponse), args.Error(1) } diff --git a/internal/tss/services/rpc_caller_service.go b/internal/tss/services/rpc_caller_service.go deleted file mode 100644 index 7f539eb..0000000 --- a/internal/tss/services/rpc_caller_service.go +++ /dev/null @@ -1,21 +0,0 @@ -package services - -import ( - "github.com/stellar/wallet-backend/internal/tss" -) - -type rpcCallerService struct { - channel tss.Channel -} - -var _ Service = (*rpcCallerService)(nil) - -func NewRPCCallerService(channel tss.Channel) Service { - return &rpcCallerService{ - channel: channel, - } -} - -func (p *rpcCallerService) ProcessPayload(payload tss.Payload) { - p.channel.Send(payload) -} diff --git a/internal/tss/services/transaction_manager.go b/internal/tss/services/transaction_manager.go new file mode 100644 index 0000000..7825582 --- /dev/null +++ b/internal/tss/services/transaction_manager.go @@ -0,0 +1,76 @@ +package services + +import ( + "context" + "fmt" + + "github.com/stellar/wallet-backend/internal/services" + "github.com/stellar/wallet-backend/internal/tss" + "github.com/stellar/wallet-backend/internal/tss/store" +) + +type TransactionManager interface { + BuildAndSubmitTransaction(ctx context.Context, channelName string, payload tss.Payload) (tss.RPCSendTxResponse, error) +} + +type TransactionManagerConfigs struct { + TxService TransactionService + RPCService services.RPCService + Store store.Store +} + +type transactionManager struct { + TxService TransactionService + RPCService services.RPCService + Store store.Store +} + +func NewTransactionManager(cfg TransactionManagerConfigs) *transactionManager { + return &transactionManager{ + TxService: cfg.TxService, + RPCService: cfg.RPCService, + Store: cfg.Store, + } +} + +func (t *transactionManager) BuildAndSubmitTransaction(ctx context.Context, channelName string, payload tss.Payload) (tss.RPCSendTxResponse, error) { + feeBumpTx, err := t.TxService.SignAndBuildNewFeeBumpTransaction(ctx, payload.TransactionXDR) + if err != nil { + return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to sign/build transaction: %w", channelName, err) + } + feeBumpTxHash, err := feeBumpTx.HashHex(t.TxService.NetworkPassphrase()) + if err != nil { + return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to hashhex fee bump transaction: %w", channelName, err) + } + + feeBumpTxXDR, err := feeBumpTx.Base64() + if err != nil { + return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to base64 fee bump transaction: %w", channelName, err) + } + + err = t.Store.UpsertTry(ctx, payload.TransactionHash, feeBumpTxHash, feeBumpTxXDR, tss.RPCTXCode{OtherCodes: tss.NewCode}) + if err != nil { + return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to upsert try in tries table: %w", channelName, err) + } + rpcResp, rpcErr := t.RPCService.SendTransaction(feeBumpTxXDR) + rpcSendResp, parseErr := tss.ParseToRPCSendTxResponse(feeBumpTxHash, rpcResp, rpcErr) + + err = t.Store.UpsertTry(ctx, payload.TransactionHash, feeBumpTxHash, feeBumpTxXDR, rpcSendResp.Code) + if err != nil { + return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to upsert try in tries table: %s", channelName, err.Error()) + } + + if parseErr != nil { + return rpcSendResp, fmt.Errorf("%s: RPC fail: %w", channelName, parseErr) + } + + if rpcErr != nil && rpcSendResp.Code.OtherCodes == tss.RPCFailCode || rpcSendResp.Code.OtherCodes == tss.UnmarshalBinaryCode { + return tss.RPCSendTxResponse{}, fmt.Errorf("%s: RPC fail: %w", channelName, rpcErr) + } + + err = t.Store.UpsertTransaction(ctx, payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, rpcSendResp.Status) + if err != nil { + return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to do the final update of tx in the transactions table: %s", channelName, err.Error()) + } + return rpcSendResp, nil +} diff --git a/internal/tss/services/transaction_manager_test.go b/internal/tss/services/transaction_manager_test.go new file mode 100644 index 0000000..5ed8d22 --- /dev/null +++ b/internal/tss/services/transaction_manager_test.go @@ -0,0 +1,195 @@ +package services + +import ( + "context" + "errors" + "testing" + + "github.com/stellar/go/xdr" + "github.com/stellar/wallet-backend/internal/db" + "github.com/stellar/wallet-backend/internal/db/dbtest" + "github.com/stellar/wallet-backend/internal/entities" + "github.com/stellar/wallet-backend/internal/services/servicesmocks" + "github.com/stellar/wallet-backend/internal/tss" + "github.com/stellar/wallet-backend/internal/tss/store" + "github.com/stellar/wallet-backend/internal/tss/utils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestBuildAndSubmitTransaction(t *testing.T) { + dbt := dbtest.Open(t) + defer dbt.Close() + + dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) + require.NoError(t, err) + defer dbConnectionPool.Close() + store := store.NewStore(dbConnectionPool) + txServiceMock := TransactionServiceMock{} + rpcServiceMock := servicesmocks.RPCServiceMock{} + txManager := NewTransactionManager(TransactionManagerConfigs{ + TxService: &txServiceMock, + RPCService: &rpcServiceMock, + Store: store, + }) + networkPass := "passphrase" + feeBumpTx := utils.BuildTestFeeBumpTransaction() + feeBumpTxXDR, _ := feeBumpTx.Base64() + feeBumpTxHash, _ := feeBumpTx.HashHex(networkPass) + payload := tss.Payload{} + payload.WebhookURL = "www.stellar.com" + payload.TransactionHash = "hash" + payload.TransactionXDR = "xdr" + + t.Run("fail_on_tx_build_and_sign", func(t *testing.T) { + _ = store.UpsertTransaction(context.Background(), payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.RPCTXStatus{OtherStatus: tss.NewStatus}) + txServiceMock. + On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). + Return(nil, errors.New("signing failed")). + Once() + + _, err := txManager.BuildAndSubmitTransaction(context.Background(), "channel", payload) + + assert.Equal(t, "channel: Unable to sign/build transaction: signing failed", err.Error()) + + var status string + err = dbConnectionPool.GetContext(context.Background(), &status, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) + require.NoError(t, err) + assert.Equal(t, string(tss.NewStatus), status) + }) + + t.Run("rpc_call_fail", func(t *testing.T) { + _ = store.UpsertTransaction(context.Background(), payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.RPCTXStatus{OtherStatus: tss.NewStatus}) + sendResp := entities.RPCSendTransactionResult{Status: string(entities.ErrorStatus)} + + txServiceMock. + On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). + Return(feeBumpTx, nil). + Once(). + On("NetworkPassphrase"). + Return(networkPass). + Once() + rpcServiceMock. + On("SendTransaction", feeBumpTxXDR). + Return(sendResp, errors.New("RPC down")). + Once() + + _, err := txManager.BuildAndSubmitTransaction(context.Background(), "channel", payload) + + assert.Equal(t, "channel: RPC fail: RPC fail: RPC down", err.Error()) + + var txStatus string + err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) + require.NoError(t, err) + assert.Equal(t, txStatus, string(tss.NewStatus)) + + var tryStatus int + err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) + require.NoError(t, err) + assert.Equal(t, int(tss.RPCFailCode), tryStatus) + }) + + t.Run("rpc_resp_empty_errorresult_xdr", func(t *testing.T) { + _ = store.UpsertTransaction(context.Background(), payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.RPCTXStatus{OtherStatus: tss.NewStatus}) + sendResp := entities.RPCSendTransactionResult{ + Status: string(entities.PendingStatus), + ErrorResultXDR: "", + } + + txServiceMock. + On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). + Return(feeBumpTx, nil). + Once(). + On("NetworkPassphrase"). + Return(networkPass). + Once() + rpcServiceMock. + On("SendTransaction", feeBumpTxXDR). + Return(sendResp, nil). + Once() + + resp, err := txManager.BuildAndSubmitTransaction(context.Background(), "channel", payload) + + assert.Equal(t, entities.PendingStatus, resp.Status.RPCStatus) + assert.Equal(t, tss.EmptyCode, resp.Code.OtherCodes) + assert.Empty(t, err) + + var txStatus string + err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) + require.NoError(t, err) + assert.Equal(t, txStatus, string(entities.PendingStatus)) + + var tryStatus int + err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) + require.NoError(t, err) + assert.Equal(t, int(tss.EmptyCode), tryStatus) + }) + t.Run("rpc_resp_has_unparsable_errorresult_xdr", func(t *testing.T) { + _ = store.UpsertTransaction(context.Background(), payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.RPCTXStatus{OtherStatus: tss.NewStatus}) + sendResp := entities.RPCSendTransactionResult{ + Status: string(entities.ErrorStatus), + ErrorResultXDR: "ABCD", + } + + txServiceMock. + On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). + Return(feeBumpTx, nil). + Once(). + On("NetworkPassphrase"). + Return(networkPass). + Once() + rpcServiceMock. + On("SendTransaction", feeBumpTxXDR). + Return(sendResp, nil). + Once() + + _, err := txManager.BuildAndSubmitTransaction(context.Background(), "channel", payload) + + assert.Equal(t, "channel: RPC fail: parse error result xdr string: unable to unmarshal errorResultXDR: ABCD", err.Error()) + + var txStatus string + err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) + require.NoError(t, err) + assert.Equal(t, txStatus, string(tss.NewStatus)) + + var tryStatus int + err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) + require.NoError(t, err) + assert.Equal(t, int(tss.UnmarshalBinaryCode), tryStatus) + }) + t.Run("rpc_returns_response", func(t *testing.T) { + _ = store.UpsertTransaction(context.Background(), payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.RPCTXStatus{OtherStatus: tss.NewStatus}) + sendResp := entities.RPCSendTransactionResult{ + Status: string(entities.ErrorStatus), + ErrorResultXDR: "AAAAAAAAAMj////9AAAAAA==", + } + + txServiceMock. + On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). + Return(feeBumpTx, nil). + Once(). + On("NetworkPassphrase"). + Return(networkPass). + Once() + rpcServiceMock. + On("SendTransaction", feeBumpTxXDR). + Return(sendResp, nil). + Once() + + resp, err := txManager.BuildAndSubmitTransaction(context.Background(), "channel", payload) + + assert.Equal(t, entities.ErrorStatus, resp.Status.RPCStatus) + assert.Equal(t, xdr.TransactionResultCodeTxTooLate, resp.Code.TxResultCode) + assert.Empty(t, err) + + var txStatus string + err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) + require.NoError(t, err) + assert.Equal(t, string(entities.ErrorStatus), txStatus) + + var tryStatus int + err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) + require.NoError(t, err) + assert.Equal(t, int(xdr.TransactionResultCodeTxTooLate), tryStatus) + }) +} diff --git a/internal/tss/utils/transaction_service.go b/internal/tss/services/transaction_service.go similarity index 50% rename from internal/tss/utils/transaction_service.go rename to internal/tss/services/transaction_service.go index 83e5643..b65bae1 100644 --- a/internal/tss/utils/transaction_service.go +++ b/internal/tss/services/transaction_service.go @@ -1,42 +1,25 @@ -package utils +package services import ( - "bytes" "context" - "encoding/base64" - "encoding/json" "fmt" - "io" - "net/http" - "strconv" - xdr3 "github.com/stellar/go-xdr/xdr3" "github.com/stellar/go/clients/horizonclient" "github.com/stellar/go/txnbuild" - "github.com/stellar/go/xdr" "github.com/stellar/wallet-backend/internal/signing" - "github.com/stellar/wallet-backend/internal/tss" tsserror "github.com/stellar/wallet-backend/internal/tss/errors" ) -type HTTPClient interface { - Post(url string, t string, body io.Reader) (resp *http.Response, err error) -} - type TransactionService interface { NetworkPassphrase() string SignAndBuildNewFeeBumpTransaction(ctx context.Context, origTxXdr string) (*txnbuild.FeeBumpTransaction, error) - SendTransaction(transactionXdr string) (tss.RPCSendTxResponse, error) - GetTransaction(transactionHash string) (tss.RPCGetIngestTxResponse, error) } type transactionService struct { DistributionAccountSignatureClient signing.SignatureClient ChannelAccountSignatureClient signing.SignatureClient HorizonClient horizonclient.ClientInterface - RPCURL string BaseFee int64 - HTTPClient HTTPClient } var _ TransactionService = (*transactionService)(nil) @@ -45,9 +28,7 @@ type TransactionServiceOptions struct { DistributionAccountSignatureClient signing.SignatureClient ChannelAccountSignatureClient signing.SignatureClient HorizonClient horizonclient.ClientInterface - RPCURL string BaseFee int64 - HTTPClient HTTPClient } func (o *TransactionServiceOptions) ValidateOptions() error { @@ -63,18 +44,10 @@ func (o *TransactionServiceOptions) ValidateOptions() error { return fmt.Errorf("horizon client cannot be nil") } - if o.RPCURL == "" { - return fmt.Errorf("rpc url cannot be empty") - } - if o.BaseFee < int64(txnbuild.MinBaseFee) { return fmt.Errorf("base fee is lower than the minimum network fee") } - if o.HTTPClient == nil { - return fmt.Errorf("http client cannot be nil") - } - return nil } @@ -86,9 +59,7 @@ func NewTransactionService(opts TransactionServiceOptions) (*transactionService, DistributionAccountSignatureClient: opts.DistributionAccountSignatureClient, ChannelAccountSignatureClient: opts.ChannelAccountSignatureClient, HorizonClient: opts.HorizonClient, - RPCURL: opts.RPCURL, BaseFee: opts.BaseFee, - HTTPClient: opts.HTTPClient, }, nil } @@ -154,85 +125,3 @@ func (t *transactionService) SignAndBuildNewFeeBumpTransaction(ctx context.Conte } return feeBumpTx, nil } - -func (t *transactionService) parseErrorResultXDR(errorResultXdr string) (tss.RPCTXCode, error) { - unMarshalErr := "unable to unmarshal errorResultXdr: %s" - decodedBytes, err := base64.StdEncoding.DecodeString(errorResultXdr) - if err != nil { - return tss.RPCTXCode{OtherCodes: tss.UnMarshalBinaryCode}, fmt.Errorf(unMarshalErr, errorResultXdr) - } - var errorResult xdr.TransactionResult - _, err = xdr3.Unmarshal(bytes.NewReader(decodedBytes), &errorResult) - if err != nil { - return tss.RPCTXCode{OtherCodes: tss.UnMarshalBinaryCode}, fmt.Errorf(unMarshalErr, errorResultXdr) - } - return tss.RPCTXCode{ - TxResultCode: errorResult.Result.Code, - }, nil -} - -func (t *transactionService) sendRPCRequest(method string, params map[string]string) (tss.RPCResponse, error) { - payload := map[string]interface{}{ - "jsonrpc": "2.0", - "id": 1, - "method": method, - "params": params, - } - jsonData, err := json.Marshal(payload) - - if err != nil { - return tss.RPCResponse{}, fmt.Errorf("marshaling payload") - } - - resp, err := t.HTTPClient.Post(t.RPCURL, "application/json", bytes.NewBuffer(jsonData)) - if err != nil { - return tss.RPCResponse{}, fmt.Errorf("%s: sending POST request to rpc: %v", method, err) - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return tss.RPCResponse{}, fmt.Errorf("%s: unmarshaling RPC response", method) - } - var res tss.RPCResponse - err = json.Unmarshal(body, &res) - if err != nil { - return tss.RPCResponse{}, fmt.Errorf("%s: parsing RPC response JSON", method) - } - if res.RPCResult == (tss.RPCResult{}) { - return tss.RPCResponse{}, fmt.Errorf("%s: response missing result field", method) - } - return res, nil -} - -func (t *transactionService) SendTransaction(transactionXdr string) (tss.RPCSendTxResponse, error) { - rpcResponse, err := t.sendRPCRequest("sendTransaction", map[string]string{"transaction": transactionXdr}) - sendTxResponse := tss.RPCSendTxResponse{} - sendTxResponse.TransactionXDR = transactionXdr - if err != nil { - sendTxResponse.Code.OtherCodes = tss.RPCFailCode - return sendTxResponse, fmt.Errorf("RPC fail: %s", err.Error()) - } - sendTxResponse.Status = tss.RPCTXStatus(rpcResponse.RPCResult.Status) - sendTxResponse.Code, err = t.parseErrorResultXDR(rpcResponse.RPCResult.ErrorResultXDR) - sendTxResponse.TransactionHash = rpcResponse.RPCResult.Hash - return sendTxResponse, err -} - -func (t *transactionService) GetTransaction(transactionHash string) (tss.RPCGetIngestTxResponse, error) { - rpcResponse, err := t.sendRPCRequest("getTransaction", map[string]string{"hash": transactionHash}) - if err != nil { - return tss.RPCGetIngestTxResponse{Status: tss.ErrorStatus}, fmt.Errorf("RPC Fail: %s", err.Error()) - } - getIngestTxResponse := tss.RPCGetIngestTxResponse{} - getIngestTxResponse.Status = tss.RPCTXStatus(rpcResponse.RPCResult.Status) - getIngestTxResponse.EnvelopeXDR = rpcResponse.RPCResult.EnvelopeXDR - getIngestTxResponse.ResultXDR = rpcResponse.RPCResult.ResultXDR - if getIngestTxResponse.Status != tss.NotFoundStatus { - getIngestTxResponse.CreatedAt, err = strconv.ParseInt(rpcResponse.RPCResult.CreatedAt, 10, 64) - if err != nil { - return tss.RPCGetIngestTxResponse{Status: tss.ErrorStatus}, fmt.Errorf("unable to parse createAt: %s", err.Error()) - } - } - return getIngestTxResponse, nil -} diff --git a/internal/tss/services/transaction_service_test.go b/internal/tss/services/transaction_service_test.go new file mode 100644 index 0000000..4dd137c --- /dev/null +++ b/internal/tss/services/transaction_service_test.go @@ -0,0 +1,236 @@ +package services + +import ( + "context" + "errors" + "testing" + + "github.com/stellar/go/clients/horizonclient" + "github.com/stellar/go/keypair" + "github.com/stellar/go/protocols/horizon" + "github.com/stellar/go/txnbuild" + "github.com/stellar/wallet-backend/internal/signing" + tsserror "github.com/stellar/wallet-backend/internal/tss/errors" + "github.com/stellar/wallet-backend/internal/tss/utils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestValidateOptions(t *testing.T) { + t.Run("return_error_when_distribution_signature_client_nil", func(t *testing.T) { + opts := TransactionServiceOptions{ + DistributionAccountSignatureClient: nil, + ChannelAccountSignatureClient: &signing.SignatureClientMock{}, + HorizonClient: &horizonclient.MockClient{}, + BaseFee: 114, + } + err := opts.ValidateOptions() + assert.Equal(t, "distribution account signature client cannot be nil", err.Error()) + + }) + + t.Run("return_error_when_channel_signature_client_nil", func(t *testing.T) { + opts := TransactionServiceOptions{ + DistributionAccountSignatureClient: &signing.SignatureClientMock{}, + ChannelAccountSignatureClient: nil, + HorizonClient: &horizonclient.MockClient{}, + BaseFee: 114, + } + err := opts.ValidateOptions() + assert.Equal(t, "channel account signature client cannot be nil", err.Error()) + }) + + t.Run("return_error_when_horizon_client_nil", func(t *testing.T) { + opts := TransactionServiceOptions{ + DistributionAccountSignatureClient: &signing.SignatureClientMock{}, + ChannelAccountSignatureClient: &signing.SignatureClientMock{}, + HorizonClient: nil, + BaseFee: 114, + } + err := opts.ValidateOptions() + assert.Equal(t, "horizon client cannot be nil", err.Error()) + }) + + t.Run("return_error_when_base_fee_too_low", func(t *testing.T) { + opts := TransactionServiceOptions{ + DistributionAccountSignatureClient: &signing.SignatureClientMock{}, + ChannelAccountSignatureClient: &signing.SignatureClientMock{}, + HorizonClient: &horizonclient.MockClient{}, + BaseFee: txnbuild.MinBaseFee - 10, + } + err := opts.ValidateOptions() + assert.Equal(t, "base fee is lower than the minimum network fee", err.Error()) + }) +} + +func TestSignAndBuildNewFeeBumpTransaction(t *testing.T) { + distributionAccountSignatureClient := signing.SignatureClientMock{} + defer distributionAccountSignatureClient.AssertExpectations(t) + channelAccountSignatureClient := signing.SignatureClientMock{} + defer channelAccountSignatureClient.AssertExpectations(t) + horizonClient := horizonclient.MockClient{} + defer horizonClient.AssertExpectations(t) + txService, _ := NewTransactionService(TransactionServiceOptions{ + DistributionAccountSignatureClient: &distributionAccountSignatureClient, + ChannelAccountSignatureClient: &channelAccountSignatureClient, + HorizonClient: &horizonClient, + BaseFee: 114, + }) + + txStr, _ := utils.BuildTestTransaction().Base64() + + t.Run("malformed_transaction_string", func(t *testing.T) { + feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), "abcd") + assert.Empty(t, feeBumpTx) + assert.ErrorIs(t, tsserror.OriginalXDRMalformed, err) + }) + + t.Run("channel_account_signature_client_get_account_public_key_err", func(t *testing.T) { + channelAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return("", errors.New("channel accounts unavailable")). + Once() + + feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) + assert.Empty(t, feeBumpTx) + assert.Equal(t, "getting channel account public key: channel accounts unavailable", err.Error()) + }) + + t.Run("horizon_client_get_account_detail_err", func(t *testing.T) { + channelAccount := keypair.MustRandom() + channelAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return(channelAccount.Address(), nil). + Once() + + horizonClient. + On("AccountDetail", horizonclient.AccountRequest{ + AccountID: channelAccount.Address(), + }). + Return(horizon.Account{}, errors.New("horizon down")). + Once() + + feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) + assert.Empty(t, feeBumpTx) + assert.Equal(t, "getting channel account details from horizon: horizon down", err.Error()) + }) + + t.Run("horizon_client_sign_stellar_transaction_w_channel_account_err", func(t *testing.T) { + channelAccount := keypair.MustRandom() + channelAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return(channelAccount.Address(), nil). + Once(). + On("SignStellarTransaction", context.Background(), mock.AnythingOfType("*txnbuild.Transaction"), []string{channelAccount.Address()}). + Return(nil, errors.New("unable to sign")). + Once() + + horizonClient. + On("AccountDetail", horizonclient.AccountRequest{ + AccountID: channelAccount.Address(), + }). + Return(horizon.Account{AccountID: channelAccount.Address(), Sequence: 1}, nil). + Once() + + feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) + assert.Empty(t, feeBumpTx) + assert.Equal(t, "signing transaction with channel account: unable to sign", err.Error()) + }) + + t.Run("distribution_account_signature_client_get_account_public_key_err", func(t *testing.T) { + channelAccount := keypair.MustRandom() + signedTx := txnbuild.Transaction{} + channelAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return(channelAccount.Address(), nil). + Once(). + On("SignStellarTransaction", context.Background(), mock.AnythingOfType("*txnbuild.Transaction"), []string{channelAccount.Address()}). + Return(&signedTx, nil). + Once() + + distributionAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return("", errors.New("client down")). + Once() + + horizonClient. + On("AccountDetail", horizonclient.AccountRequest{ + AccountID: channelAccount.Address(), + }). + Return(horizon.Account{AccountID: channelAccount.Address(), Sequence: 1}, nil). + Once() + + feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) + assert.Empty(t, feeBumpTx) + assert.Equal(t, "getting distribution account public key: client down", err.Error()) + }) + + t.Run("horizon_client_sign_stellar_transaction_w_distribition_account_err", func(t *testing.T) { + account := keypair.MustRandom() + signedTx := utils.BuildTestTransaction() + channelAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return(account.Address(), nil). + Once(). + On("SignStellarTransaction", context.Background(), mock.AnythingOfType("*txnbuild.Transaction"), []string{account.Address()}). + Return(signedTx, nil). + Once() + + distributionAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return(account.Address(), nil). + Once(). + On("SignStellarFeeBumpTransaction", context.Background(), mock.AnythingOfType("*txnbuild.FeeBumpTransaction")). + Return(nil, errors.New("unable to sign")). + Once() + + horizonClient. + On("AccountDetail", horizonclient.AccountRequest{ + AccountID: account.Address(), + }). + Return(horizon.Account{AccountID: account.Address(), Sequence: 1}, nil). + Once() + + feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) + assert.Empty(t, feeBumpTx) + assert.Equal(t, "signing the fee bump transaction with distribution account: unable to sign", err.Error()) + }) + + t.Run("returns_signed_tx", func(t *testing.T) { + account := keypair.MustRandom() + signedTx := utils.BuildTestTransaction() + testFeeBumpTx, _ := txnbuild.NewFeeBumpTransaction( + txnbuild.FeeBumpTransactionParams{ + Inner: signedTx, + FeeAccount: account.Address(), + BaseFee: int64(100), + }, + ) + channelAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return(account.Address(), nil). + Once(). + On("SignStellarTransaction", context.Background(), mock.AnythingOfType("*txnbuild.Transaction"), []string{account.Address()}). + Return(signedTx, nil). + Once() + + distributionAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return(account.Address(), nil). + Once(). + On("SignStellarFeeBumpTransaction", context.Background(), mock.AnythingOfType("*txnbuild.FeeBumpTransaction")). + Return(testFeeBumpTx, nil). + Once() + + horizonClient. + On("AccountDetail", horizonclient.AccountRequest{ + AccountID: account.Address(), + }). + Return(horizon.Account{AccountID: account.Address(), Sequence: 1}, nil). + Once() + + feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) + assert.Equal(t, feeBumpTx, testFeeBumpTx) + assert.Empty(t, err) + }) +} diff --git a/internal/tss/services/types.go b/internal/tss/services/types.go deleted file mode 100644 index f395190..0000000 --- a/internal/tss/services/types.go +++ /dev/null @@ -1,7 +0,0 @@ -package services - -import "github.com/stellar/wallet-backend/internal/tss" - -type Service interface { - ProcessPayload(payload tss.Payload) -} diff --git a/internal/tss/services/webhook_handler_service.go b/internal/tss/services/webhook_handler_service.go deleted file mode 100644 index 33c837e..0000000 --- a/internal/tss/services/webhook_handler_service.go +++ /dev/null @@ -1,19 +0,0 @@ -package services - -import ( - "github.com/stellar/wallet-backend/internal/tss" -) - -type webhookHandlerService struct { - channel tss.Channel -} - -func NewWebhookHandlerService(channel tss.Channel) Service { - return &webhookHandlerService{ - channel: channel, - } -} - -func (p *webhookHandlerService) ProcessPayload(payload tss.Payload) { - // fill in later -} diff --git a/internal/tss/store/store.go b/internal/tss/store/store.go index 866f1cc..b7953b0 100644 --- a/internal/tss/store/store.go +++ b/internal/tss/store/store.go @@ -38,7 +38,7 @@ func (s *store) UpsertTransaction(ctx context.Context, webhookURL string, txHash current_status = $4, updated_at = NOW(); ` - _, err := s.DB.ExecContext(ctx, q, txHash, txXDR, webhookURL, string(status)) + _, err := s.DB.ExecContext(ctx, q, txHash, txXDR, webhookURL, status.Status()) if err != nil { return fmt.Errorf("inserting/updatig tss transaction: %w", err) } diff --git a/internal/tss/store/store_test.go b/internal/tss/store/store_test.go index 57709ab..2987a27 100644 --- a/internal/tss/store/store_test.go +++ b/internal/tss/store/store_test.go @@ -7,6 +7,7 @@ import ( "github.com/stellar/go/xdr" "github.com/stellar/wallet-backend/internal/db" "github.com/stellar/wallet-backend/internal/db/dbtest" + "github.com/stellar/wallet-backend/internal/entities" "github.com/stellar/wallet-backend/internal/tss" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -20,7 +21,7 @@ func TestUpsertTransaction(t *testing.T) { defer dbConnectionPool.Close() store := NewStore(dbConnectionPool) t.Run("insert", func(t *testing.T) { - _ = store.UpsertTransaction(context.Background(), "www.stellar.org", "hash", "xdr", tss.NewStatus) + _ = store.UpsertTransaction(context.Background(), "www.stellar.org", "hash", "xdr", tss.RPCTXStatus{OtherStatus: tss.NewStatus}) var status string err = dbConnectionPool.GetContext(context.Background(), &status, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, "hash") @@ -29,13 +30,13 @@ func TestUpsertTransaction(t *testing.T) { }) t.Run("update", func(t *testing.T) { - _ = store.UpsertTransaction(context.Background(), "www.stellar.org", "hash", "xdr", tss.NewStatus) - _ = store.UpsertTransaction(context.Background(), "www.stellar.org", "hash", "xdr", tss.SuccessStatus) + _ = store.UpsertTransaction(context.Background(), "www.stellar.org", "hash", "xdr", tss.RPCTXStatus{OtherStatus: tss.NewStatus}) + _ = store.UpsertTransaction(context.Background(), "www.stellar.org", "hash", "xdr", tss.RPCTXStatus{RPCStatus: entities.SuccessStatus}) var status string err = dbConnectionPool.GetContext(context.Background(), &status, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, "hash") require.NoError(t, err) - assert.Equal(t, status, string(tss.SuccessStatus)) + assert.Equal(t, status, string(entities.SuccessStatus)) var numRows int err = dbConnectionPool.GetContext(context.Background(), &numRows, `SELECT count(*) FROM tss_transactions WHERE transaction_hash = $1`, "hash") diff --git a/internal/tss/types.go b/internal/tss/types.go index 372dc6e..6104f6a 100644 --- a/internal/tss/types.go +++ b/internal/tss/types.go @@ -1,19 +1,77 @@ package tss -import "github.com/stellar/go/xdr" +import ( + "bytes" + "encoding/base64" + "fmt" + "strconv" -type RPCTXStatus string + xdr3 "github.com/stellar/go-xdr/xdr3" + "github.com/stellar/go/xdr" + "github.com/stellar/wallet-backend/internal/entities" +) + +type RPCGetIngestTxResponse struct { + // A status that indicated whether this transaction failed or successly made it to the ledger + Status entities.RPCStatus + // The raw TransactionEnvelope XDR for this transaction + EnvelopeXDR string + // The raw TransactionResult XDR of the envelopeXdr + ResultXDR string + // The unix timestamp of when the transaction was included in the ledger + CreatedAt int64 +} + +//nolint:unused +func ParseToRPCGetIngestTxResponse(result entities.RPCGetTransactionResult, err error) (RPCGetIngestTxResponse, error) { + if err != nil { + return RPCGetIngestTxResponse{Status: entities.ErrorStatus}, err + } + + getIngestTxResponse := RPCGetIngestTxResponse{ + Status: entities.RPCStatus(result.Status), + EnvelopeXDR: result.EnvelopeXDR, + ResultXDR: result.ResultXDR, + } + if getIngestTxResponse.Status != entities.NotFoundStatus { + getIngestTxResponse.CreatedAt, err = strconv.ParseInt(result.CreatedAt, 10, 64) + if err != nil { + return RPCGetIngestTxResponse{Status: entities.ErrorStatus}, fmt.Errorf("unable to parse createdAt: %w", err) + } + } + return getIngestTxResponse, nil +} + +type OtherStatus string type OtherCodes int32 type TransactionResultCode int32 +const ( + NewStatus OtherStatus = "NEW" + NoStatus OtherStatus = "" +) + +type RPCTXStatus struct { + RPCStatus entities.RPCStatus + OtherStatus OtherStatus +} + +func (s RPCTXStatus) Status() string { + if s.OtherStatus != NoStatus { + return string(s.OtherStatus) + } + return string(s.RPCStatus) +} + const ( // Do not use NoCode NoCode OtherCodes = 0 // These values need to not overlap the values in xdr.TransactionResultCode NewCode OtherCodes = 100 RPCFailCode OtherCodes = 101 - UnMarshalBinaryCode OtherCodes = 102 + UnmarshalBinaryCode OtherCodes = 102 + EmptyCode OtherCodes = 103 ) type RPCTXCode struct { @@ -28,19 +86,14 @@ func (c RPCTXCode) Code() int { return int(c.TxResultCode) } -const ( - // Brand new transaction, not sent to RPC yet - NewStatus RPCTXStatus = "NEW" - // RPC sendTransaction statuses - PendingStatus RPCTXStatus = "PENDING" - DuplicateStatus RPCTXStatus = "DUPLICATE" - TryAgainLaterStatus RPCTXStatus = "TRY_AGAIN_LATER" - ErrorStatus RPCTXStatus = "ERROR" - // RPC getTransaction(s) statuses - NotFoundStatus RPCTXStatus = "NOT_FOUND" - FailedStatus RPCTXStatus = "FAILED" - SuccessStatus RPCTXStatus = "SUCCESS" -) +var FinalErrorCodes = []xdr.TransactionResultCode{ + xdr.TransactionResultCodeTxSuccess, + xdr.TransactionResultCodeTxFailed, + xdr.TransactionResultCodeTxMissingOperation, + xdr.TransactionResultCodeTxInsufficientBalance, + xdr.TransactionResultCodeTxBadAuthExtra, + xdr.TransactionResultCodeTxMalformed, +} var NonJitterErrorCodes = []xdr.TransactionResultCode{ xdr.TransactionResultCodeTxTooEarly, @@ -53,17 +106,6 @@ var JitterErrorCodes = []xdr.TransactionResultCode{ xdr.TransactionResultCodeTxInternalError, } -type RPCGetIngestTxResponse struct { - // A status that indicated whether this transaction failed or successly made it to the ledger - Status RPCTXStatus - // The raw TransactionEnvelope XDR for this transaction - EnvelopeXDR string - // The raw TransactionResult XDR of the envelopeXdr - ResultXDR string - // The unix timestamp of when the transaction was included in the ledger - CreatedAt int64 -} - type RPCSendTxResponse struct { // The hash of the transaction submitted to RPC TransactionHash string @@ -75,6 +117,46 @@ type RPCSendTxResponse struct { Code RPCTXCode } +func ParseToRPCSendTxResponse(transactionXDR string, result entities.RPCSendTransactionResult, err error) (RPCSendTxResponse, error) { + sendTxResponse := RPCSendTxResponse{} + sendTxResponse.TransactionXDR = transactionXDR + if err != nil { + fmt.Println("RPC FAIL ON THIS?") + sendTxResponse.Status.RPCStatus = entities.ErrorStatus + sendTxResponse.Code.OtherCodes = RPCFailCode + return sendTxResponse, fmt.Errorf("RPC fail: %w", err) + } + sendTxResponse.Status.RPCStatus = entities.RPCStatus(result.Status) + sendTxResponse.TransactionHash = result.Hash + fmt.Println("abt to call parse") + sendTxResponse.Code, err = parseSendTransactionErrorXDR(result.ErrorResultXDR) + if err != nil { + return sendTxResponse, fmt.Errorf("parse error result xdr string: %w", err) + } + return sendTxResponse, nil +} + +func parseSendTransactionErrorXDR(errorResultXDR string) (RPCTXCode, error) { + if errorResultXDR == "" { + return RPCTXCode{ + OtherCodes: EmptyCode, + }, nil + } + unmarshalErr := "unable to unmarshal errorResultXDR: %s" + decodedBytes, err := base64.StdEncoding.DecodeString(errorResultXDR) + if err != nil { + return RPCTXCode{OtherCodes: UnmarshalBinaryCode}, fmt.Errorf(unmarshalErr, errorResultXDR) + } + var errorResult xdr.TransactionResult + _, err = xdr3.Unmarshal(bytes.NewReader(decodedBytes), &errorResult) + if err != nil { + return RPCTXCode{OtherCodes: UnmarshalBinaryCode}, fmt.Errorf(unmarshalErr, errorResultXDR) + } + return RPCTXCode{ + TxResultCode: errorResult.Result.Code, + }, nil +} + type Payload struct { WebhookURL string // The hash of the transaction xdr submitted by the client - the id of the transaction submitted by a client @@ -87,19 +169,6 @@ type Payload struct { RpcGetIngestTxResponse RPCGetIngestTxResponse } -type RPCResult struct { - Status string `json:"status"` - EnvelopeXDR string `json:"envelopeXdr"` - ResultXDR string `json:"resultXdr"` - ErrorResultXDR string `json:"errorResultXdr"` - Hash string `json:"hash"` - CreatedAt string `json:"createdAt"` -} - -type RPCResponse struct { - RPCResult `json:"result"` -} - type Channel interface { Send(payload Payload) Receive(payload Payload) diff --git a/internal/tss/utils/mocks.go b/internal/tss/utils/mocks.go deleted file mode 100644 index 597a6c9..0000000 --- a/internal/tss/utils/mocks.go +++ /dev/null @@ -1,50 +0,0 @@ -package utils - -import ( - "context" - "io" - "net/http" - - "github.com/stellar/go/txnbuild" - "github.com/stellar/wallet-backend/internal/tss" - "github.com/stretchr/testify/mock" -) - -type MockHTTPClient struct { - mock.Mock -} - -func (s *MockHTTPClient) Post(url, contentType string, body io.Reader) (resp *http.Response, err error) { - args := s.Called(url, contentType, body) - return args.Get(0).(*http.Response), args.Error(1) -} - -type TransactionServiceMock struct { - mock.Mock -} - -var _ TransactionService = (*TransactionServiceMock)(nil) - -func (t *TransactionServiceMock) NetworkPassphrase() string { - args := t.Called() - return args.String(0) -} - -func (t *TransactionServiceMock) SignAndBuildNewFeeBumpTransaction(ctx context.Context, origTxXdr string) (*txnbuild.FeeBumpTransaction, error) { - args := t.Called(ctx, origTxXdr) - if result := args.Get(0); result != nil { - return result.(*txnbuild.FeeBumpTransaction), args.Error(1) - } - return nil, args.Error(1) - -} - -func (t *TransactionServiceMock) SendTransaction(transactionXdr string) (tss.RPCSendTxResponse, error) { - args := t.Called(transactionXdr) - return args.Get(0).(tss.RPCSendTxResponse), args.Error(1) -} - -func (t *TransactionServiceMock) GetTransaction(transactionHash string) (tss.RPCGetIngestTxResponse, error) { - args := t.Called(transactionHash) - return args.Get(0).(tss.RPCGetIngestTxResponse), args.Error(1) -} diff --git a/internal/tss/utils/transaction_service_test.go b/internal/tss/utils/transaction_service_test.go deleted file mode 100644 index f924f01..0000000 --- a/internal/tss/utils/transaction_service_test.go +++ /dev/null @@ -1,596 +0,0 @@ -package utils - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "strings" - "testing" - - "github.com/stellar/go/clients/horizonclient" - "github.com/stellar/go/keypair" - "github.com/stellar/go/protocols/horizon" - "github.com/stellar/go/txnbuild" - "github.com/stellar/go/xdr" - "github.com/stellar/wallet-backend/internal/signing" - "github.com/stellar/wallet-backend/internal/tss" - tsserror "github.com/stellar/wallet-backend/internal/tss/errors" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" -) - -func TestValidateOptions(t *testing.T) { - t.Run("return_error_when_distribution_signature_client_nil", func(t *testing.T) { - opts := TransactionServiceOptions{ - DistributionAccountSignatureClient: nil, - ChannelAccountSignatureClient: &signing.SignatureClientMock{}, - HorizonClient: &horizonclient.MockClient{}, - RPCURL: "http://localhost:8000/soroban/rpc", - BaseFee: 114, - HTTPClient: &MockHTTPClient{}, - } - err := opts.ValidateOptions() - assert.Equal(t, "distribution account signature client cannot be nil", err.Error()) - - }) - - t.Run("return_error_when_channel_signature_client_nil", func(t *testing.T) { - opts := TransactionServiceOptions{ - DistributionAccountSignatureClient: &signing.SignatureClientMock{}, - ChannelAccountSignatureClient: nil, - HorizonClient: &horizonclient.MockClient{}, - RPCURL: "http://localhost:8000/soroban/rpc", - BaseFee: 114, - HTTPClient: &MockHTTPClient{}, - } - err := opts.ValidateOptions() - assert.Equal(t, "channel account signature client cannot be nil", err.Error()) - }) - - t.Run("return_error_when_horizon_client_nil", func(t *testing.T) { - opts := TransactionServiceOptions{ - DistributionAccountSignatureClient: &signing.SignatureClientMock{}, - ChannelAccountSignatureClient: &signing.SignatureClientMock{}, - HorizonClient: nil, - RPCURL: "http://localhost:8000/soroban/rpc", - BaseFee: 114, - HTTPClient: &MockHTTPClient{}, - } - err := opts.ValidateOptions() - assert.Equal(t, "horizon client cannot be nil", err.Error()) - }) - - t.Run("return_error_when_rpc_url_empty", func(t *testing.T) { - opts := TransactionServiceOptions{ - DistributionAccountSignatureClient: &signing.SignatureClientMock{}, - ChannelAccountSignatureClient: &signing.SignatureClientMock{}, - HorizonClient: &horizonclient.MockClient{}, - RPCURL: "", - BaseFee: 114, - HTTPClient: &MockHTTPClient{}, - } - err := opts.ValidateOptions() - assert.Equal(t, "rpc url cannot be empty", err.Error()) - }) - - t.Run("return_error_when_base_fee_too_low", func(t *testing.T) { - opts := TransactionServiceOptions{ - DistributionAccountSignatureClient: &signing.SignatureClientMock{}, - ChannelAccountSignatureClient: &signing.SignatureClientMock{}, - HorizonClient: &horizonclient.MockClient{}, - RPCURL: "http://localhost:8000/soroban/rpc", - BaseFee: txnbuild.MinBaseFee - 10, - HTTPClient: &MockHTTPClient{}, - } - err := opts.ValidateOptions() - assert.Equal(t, "base fee is lower than the minimum network fee", err.Error()) - }) - - t.Run("return_error_http_client_nil", func(t *testing.T) { - opts := TransactionServiceOptions{ - DistributionAccountSignatureClient: &signing.SignatureClientMock{}, - ChannelAccountSignatureClient: &signing.SignatureClientMock{}, - HorizonClient: &horizonclient.MockClient{}, - RPCURL: "http://localhost:8000/soroban/rpc", - BaseFee: 114, - } - err := opts.ValidateOptions() - assert.Equal(t, "http client cannot be nil", err.Error()) - }) -} - -func TestSignAndBuildNewFeeBumpTransaction(t *testing.T) { - distributionAccountSignatureClient := signing.SignatureClientMock{} - defer distributionAccountSignatureClient.AssertExpectations(t) - channelAccountSignatureClient := signing.SignatureClientMock{} - defer channelAccountSignatureClient.AssertExpectations(t) - horizonClient := horizonclient.MockClient{} - defer horizonClient.AssertExpectations(t) - txService, _ := NewTransactionService(TransactionServiceOptions{ - DistributionAccountSignatureClient: &distributionAccountSignatureClient, - ChannelAccountSignatureClient: &channelAccountSignatureClient, - HorizonClient: &horizonClient, - RPCURL: "http://localhost:8000/soroban/rpc", - BaseFee: 114, - HTTPClient: &MockHTTPClient{}, - }) - - txStr, _ := BuildTestTransaction().Base64() - - t.Run("malformed_transaction_string", func(t *testing.T) { - feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), "abcd") - assert.Empty(t, feeBumpTx) - assert.ErrorIs(t, tsserror.OriginalXDRMalformed, err) - }) - - t.Run("channel_account_signature_client_get_account_public_key_err", func(t *testing.T) { - channelAccountSignatureClient. - On("GetAccountPublicKey", context.Background()). - Return("", errors.New("channel accounts unavailable")). - Once() - - feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) - assert.Empty(t, feeBumpTx) - assert.Equal(t, "getting channel account public key: channel accounts unavailable", err.Error()) - }) - - t.Run("horizon_client_get_account_detail_err", func(t *testing.T) { - channelAccount := keypair.MustRandom() - channelAccountSignatureClient. - On("GetAccountPublicKey", context.Background()). - Return(channelAccount.Address(), nil). - Once() - - horizonClient. - On("AccountDetail", horizonclient.AccountRequest{ - AccountID: channelAccount.Address(), - }). - Return(horizon.Account{}, errors.New("horizon down")). - Once() - - feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) - assert.Empty(t, feeBumpTx) - assert.Equal(t, "getting channel account details from horizon: horizon down", err.Error()) - }) - - t.Run("horizon_client_sign_stellar_transaction_w_channel_account_err", func(t *testing.T) { - channelAccount := keypair.MustRandom() - channelAccountSignatureClient. - On("GetAccountPublicKey", context.Background()). - Return(channelAccount.Address(), nil). - Once(). - On("SignStellarTransaction", context.Background(), mock.AnythingOfType("*txnbuild.Transaction"), []string{channelAccount.Address()}). - Return(nil, errors.New("unable to sign")). - Once() - - horizonClient. - On("AccountDetail", horizonclient.AccountRequest{ - AccountID: channelAccount.Address(), - }). - Return(horizon.Account{AccountID: channelAccount.Address(), Sequence: 1}, nil). - Once() - - feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) - assert.Empty(t, feeBumpTx) - assert.Equal(t, "signing transaction with channel account: unable to sign", err.Error()) - }) - - t.Run("distribution_account_signature_client_get_account_public_key_err", func(t *testing.T) { - channelAccount := keypair.MustRandom() - signedTx := txnbuild.Transaction{} - channelAccountSignatureClient. - On("GetAccountPublicKey", context.Background()). - Return(channelAccount.Address(), nil). - Once(). - On("SignStellarTransaction", context.Background(), mock.AnythingOfType("*txnbuild.Transaction"), []string{channelAccount.Address()}). - Return(&signedTx, nil). - Once() - - distributionAccountSignatureClient. - On("GetAccountPublicKey", context.Background()). - Return("", errors.New("client down")). - Once() - - horizonClient. - On("AccountDetail", horizonclient.AccountRequest{ - AccountID: channelAccount.Address(), - }). - Return(horizon.Account{AccountID: channelAccount.Address(), Sequence: 1}, nil). - Once() - - feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) - assert.Empty(t, feeBumpTx) - assert.Equal(t, "getting distribution account public key: client down", err.Error()) - }) - - t.Run("horizon_client_sign_stellar_transaction_w_distribition_account_err", func(t *testing.T) { - account := keypair.MustRandom() - signedTx := BuildTestTransaction() - channelAccountSignatureClient. - On("GetAccountPublicKey", context.Background()). - Return(account.Address(), nil). - Once(). - On("SignStellarTransaction", context.Background(), mock.AnythingOfType("*txnbuild.Transaction"), []string{account.Address()}). - Return(signedTx, nil). - Once() - - distributionAccountSignatureClient. - On("GetAccountPublicKey", context.Background()). - Return(account.Address(), nil). - Once(). - On("SignStellarFeeBumpTransaction", context.Background(), mock.AnythingOfType("*txnbuild.FeeBumpTransaction")). - Return(nil, errors.New("unable to sign")). - Once() - - horizonClient. - On("AccountDetail", horizonclient.AccountRequest{ - AccountID: account.Address(), - }). - Return(horizon.Account{AccountID: account.Address(), Sequence: 1}, nil). - Once() - - feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) - assert.Empty(t, feeBumpTx) - assert.Equal(t, "signing the fee bump transaction with distribution account: unable to sign", err.Error()) - }) - - t.Run("returns_signed_tx", func(t *testing.T) { - account := keypair.MustRandom() - signedTx := BuildTestTransaction() - testFeeBumpTx, _ := txnbuild.NewFeeBumpTransaction( - txnbuild.FeeBumpTransactionParams{ - Inner: signedTx, - FeeAccount: account.Address(), - BaseFee: int64(100), - }, - ) - channelAccountSignatureClient. - On("GetAccountPublicKey", context.Background()). - Return(account.Address(), nil). - Once(). - On("SignStellarTransaction", context.Background(), mock.AnythingOfType("*txnbuild.Transaction"), []string{account.Address()}). - Return(signedTx, nil). - Once() - - distributionAccountSignatureClient. - On("GetAccountPublicKey", context.Background()). - Return(account.Address(), nil). - Once(). - On("SignStellarFeeBumpTransaction", context.Background(), mock.AnythingOfType("*txnbuild.FeeBumpTransaction")). - Return(testFeeBumpTx, nil). - Once() - - horizonClient. - On("AccountDetail", horizonclient.AccountRequest{ - AccountID: account.Address(), - }). - Return(horizon.Account{AccountID: account.Address(), Sequence: 1}, nil). - Once() - - feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) - assert.Equal(t, feeBumpTx, testFeeBumpTx) - assert.Empty(t, err) - }) -} - -type errorReader struct{} - -func (e *errorReader) Read(p []byte) (n int, err error) { - return 0, fmt.Errorf("read error") -} - -func (e *errorReader) Close() error { - return nil -} - -func TestSendRPCRequest(t *testing.T) { - mockHTTPClient := MockHTTPClient{} - rpcURL := "http://localhost:8000/soroban/rpc" - txService, _ := NewTransactionService(TransactionServiceOptions{ - DistributionAccountSignatureClient: &signing.SignatureClientMock{}, - ChannelAccountSignatureClient: &signing.SignatureClientMock{}, - HorizonClient: &horizonclient.MockClient{}, - RPCURL: rpcURL, - BaseFee: 114, - HTTPClient: &mockHTTPClient, - }) - method := "sendTransaction" - params := map[string]string{"transaction": "ABCD"} - payload := map[string]interface{}{ - "jsonrpc": "2.0", - "id": 1, - "method": method, - "params": params, - } - jsonData, _ := json.Marshal(payload) - t.Run("rpc_post_call_fails", func(t *testing.T) { - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(&http.Response{}, errors.New("RPC Connection fail")). - Once() - - resp, err := txService.sendRPCRequest(method, params) - - assert.Empty(t, resp) - assert.Equal(t, "sendTransaction: sending POST request to rpc: RPC Connection fail", err.Error()) - }) - - t.Run("unmarshaling_rpc_response_fails", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(&errorReader{}), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.sendRPCRequest(method, params) - - assert.Empty(t, resp) - assert.Equal(t, "sendTransaction: unmarshaling RPC response", err.Error()) - }) - - t.Run("unmarshaling_json_fails", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{invalid-json`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.sendRPCRequest(method, params) - - assert.Empty(t, resp) - assert.Equal(t, "sendTransaction: parsing RPC response JSON", err.Error()) - }) - - t.Run("response_has_no_result_field", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"status": "success"}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.sendRPCRequest(method, params) - - assert.Empty(t, resp) - assert.Equal(t, "sendTransaction: response missing result field", err.Error()) - }) - - t.Run("response_has_status_field", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"status": "PENDING"}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.sendRPCRequest(method, params) - - assert.Equal(t, "PENDING", resp.Status) - assert.Empty(t, err) - }) - - t.Run("response_has_envelopexdr_field", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"envelopeXdr": "exdr"}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.sendRPCRequest(method, params) - - assert.Equal(t, "exdr", resp.EnvelopeXDR) - assert.Empty(t, err) - }) - - t.Run("response_has_resultxdr_field", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"resultXdr": "rxdr"}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.sendRPCRequest(method, params) - - assert.Equal(t, "rxdr", resp.ResultXDR) - assert.Empty(t, err) - }) - - t.Run("response_has_errorresultxdr_field", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"errorResultXdr": "exdr"}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.sendRPCRequest(method, params) - - assert.Equal(t, "exdr", resp.ErrorResultXDR) - assert.Empty(t, err) - }) - - t.Run("response_has_hash_field", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"hash": "hash"}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.sendRPCRequest(method, params) - - assert.Equal(t, "hash", resp.Hash) - assert.Empty(t, err) - }) - - t.Run("response_has_createdat_field", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"createdAt": "1234"}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.sendRPCRequest(method, params) - - assert.Equal(t, "1234", resp.CreatedAt) - assert.Empty(t, err) - }) -} - -func TestSendTransaction(t *testing.T) { - mockHTTPClient := MockHTTPClient{} - rpcURL := "http://localhost:8000/soroban/rpc" - txService, _ := NewTransactionService(TransactionServiceOptions{ - DistributionAccountSignatureClient: &signing.SignatureClientMock{}, - ChannelAccountSignatureClient: &signing.SignatureClientMock{}, - HorizonClient: &horizonclient.MockClient{}, - RPCURL: rpcURL, - BaseFee: 114, - HTTPClient: &mockHTTPClient, - }) - method := "sendTransaction" - params := map[string]string{"transaction": "ABCD"} - payload := map[string]interface{}{ - "jsonrpc": "2.0", - "id": 1, - "method": method, - "params": params, - } - jsonData, _ := json.Marshal(payload) - - t.Run("rpc_request_fails", func(t *testing.T) { - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(&http.Response{}, errors.New("RPC Connection fail")). - Once() - - resp, err := txService.SendTransaction("ABCD") - - assert.Equal(t, tss.RPCFailCode, resp.Code.OtherCodes) - assert.Equal(t, "RPC fail: sendTransaction: sending POST request to rpc: RPC Connection fail", err.Error()) - - }) - t.Run("response_has_unparsable_errorResultXdr", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"errorResultXdr": "ABC123"}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.SendTransaction("ABCD") - - assert.Equal(t, tss.UnMarshalBinaryCode, resp.Code.OtherCodes) - assert.Equal(t, "unable to unmarshal errorResultXdr: ABC123", err.Error()) - }) - t.Run("response_has_errorResultXdr", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"errorResultXdr": "AAAAAAAAAMj////9AAAAAA=="}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.SendTransaction("ABCD") - - assert.Equal(t, xdr.TransactionResultCodeTxTooLate, resp.Code.TxResultCode) - assert.Empty(t, err) - }) -} - -func TestGetTransaction(t *testing.T) { - mockHTTPClient := MockHTTPClient{} - rpcURL := "http://localhost:8000/soroban/rpc" - txService, _ := NewTransactionService(TransactionServiceOptions{ - DistributionAccountSignatureClient: &signing.SignatureClientMock{}, - ChannelAccountSignatureClient: &signing.SignatureClientMock{}, - HorizonClient: &horizonclient.MockClient{}, - RPCURL: rpcURL, - BaseFee: 114, - HTTPClient: &mockHTTPClient, - }) - method := "getTransaction" - params := map[string]string{"hash": "XYZ"} - payload := map[string]interface{}{ - "jsonrpc": "2.0", - "id": 1, - "method": method, - "params": params, - } - jsonData, _ := json.Marshal(payload) - - t.Run("rpc_request_fails", func(t *testing.T) { - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(&http.Response{}, errors.New("RPC Connection fail")). - Once() - - resp, err := txService.GetTransaction("XYZ") - - assert.Equal(t, tss.ErrorStatus, resp.Status) - assert.Equal(t, "RPC Fail: getTransaction: sending POST request to rpc: RPC Connection fail", err.Error()) - - }) - t.Run("unable_to_parse_createdAt", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"status": "SUCCESS", "createdAt": "ABCD"}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.GetTransaction("XYZ") - - assert.Equal(t, tss.ErrorStatus, resp.Status) - assert.Equal(t, "unable to parse createAt: strconv.ParseInt: parsing \"ABCD\": invalid syntax", err.Error()) - }) - t.Run("response_has_createdAt_field", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"createdAt": "1234567"}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.GetTransaction("XYZ") - - assert.Equal(t, int64(1234567), resp.CreatedAt) - assert.Empty(t, err) - }) - -} diff --git a/internal/utils/http_client.go b/internal/utils/http_client.go new file mode 100644 index 0000000..514abf0 --- /dev/null +++ b/internal/utils/http_client.go @@ -0,0 +1,21 @@ +package utils + +import ( + "io" + "net/http" + + "github.com/stretchr/testify/mock" +) + +type HTTPClient interface { + Post(url string, t string, body io.Reader) (resp *http.Response, err error) +} + +type MockHTTPClient struct { + mock.Mock +} + +func (s *MockHTTPClient) Post(url, contentType string, body io.Reader) (resp *http.Response, err error) { + args := s.Called(url, contentType, body) + return args.Get(0).(*http.Response), args.Error(1) +} From 3b174b80b7297338e68c0436a1d79c37b031ff43 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Thu, 26 Sep 2024 01:27:48 -0700 Subject: [PATCH 070/113] changes to serve.go --- internal/serve/serve.go | 74 +++++++++++++++++------------------------ 1 file changed, 31 insertions(+), 43 deletions(-) diff --git a/internal/serve/serve.go b/internal/serve/serve.go index e2466ee..f2b2f1e 100644 --- a/internal/serve/serve.go +++ b/internal/serve/serve.go @@ -91,9 +91,11 @@ type handlerDeps struct { AccountSponsorshipService services.AccountSponsorshipService PaymentService services.PaymentService // TSS - RPCCallerServiceChannel tss.Channel - TSSRouter tssrouter.Router - AppTracker apptracker.AppTracker + RPCCallerChannel tss.Channel + ErrorJitterChannel tss.Channel + ErrorNonJitterChannel tss.Channel + TSSRouter tssrouter.Router + AppTracker apptracker.AppTracker } func Serve(cfg Configs) error { @@ -111,9 +113,9 @@ func Serve(cfg Configs) error { }, OnStopping: func() { log.Info("Stopping Wallet Backend server") - deps.ErrorHandlerServiceJitterChannel.Stop() - deps.ErrorHandlerServiceNonJitterChannel.Stop() - deps.RPCCallerServiceChannel.Stop() + deps.ErrorJitterChannel.Stop() + deps.ErrorNonJitterChannel.Stop() + deps.RPCCallerChannel.Stop() }, }) @@ -200,60 +202,44 @@ func initHandlerDeps(cfg Configs) (handlerDeps, error) { RPCService: rpcService, Store: store, }) - tssChannelConfigs := tsschannel.RPCCallerChannelConfigs{ + rpcCallerChannelConfigs := tsschannel.RPCCallerChannelConfigs{ TxManager: txManager, Store: store, MaxBufferSize: cfg.RPCCallerServiceChannelBufferSize, MaxWorkers: cfg.RPCCallerServiceChannelMaxWorkers, } - rpcCallerServiceChannel := tsschannel.NewRPCCallerChannel(tssChannelConfigs) + rpcCallerChannel := tsschannel.NewRPCCallerChannel(rpcCallerChannelConfigs) - router := tssrouter.NewRouter(tssrouter.RouterConfigs{ - RPCCallerChannel: rpcCallerServiceChannel, - ErrorJitterChannel: nil, - ErrorNonJitterChannel: nil, - WebhookChannel: nil, - }) - - rpcCallerServiceChannel.SetRouter(router) - - jitterChannelOpts := tsschannel.RPCErrorHandlerServiceJitterChannelConfigs{ - Store: store, - TxService: tssTxService, + errorJitterChannelConfigs := tsschannel.ErrorJitterChannelConfigs{ + TxManager: txManager, MaxBufferSize: cfg.ErrorHandlerServiceJitterChannelBufferSize, MaxWorkers: cfg.ErrorHandlerServiceJitterChannelMaxWorkers, MaxRetries: cfg.ErrorHandlerServiceJitterChannelMaxRetries, MinWaitBtwnRetriesMS: cfg.ErrorHandlerServiceJitterChannelMinWaitBtwnRetriesMS, } - jitterChannel := tsschannel.NewErrorHandlerServiceJitterChannel(jitterChannelOpts) + errorJitterChannel := tsschannel.NewErrorJitterChannel(errorJitterChannelConfigs) - nonJitterChannelOpts := tsschannel.RPCErrorHandlerServiceNonJitterChannelConfigs{ - Store: store, - TxService: tssTxService, - MaxBufferSize: cfg.ErrorHandlerServiceNonJitterChannelBufferSize, - MaxWorkers: cfg.ErrorHandlerServiceNonJitterChannelMaxWorkers, - MaxRetries: cfg.ErrorHandlerServiceNonJitterChannelMaxRetries, - WaitBtwnRetriesMS: cfg.ErrorHandlerServiceNonJitterChannelWaitBtwnRetriesMS, + errorNonJitterChannelConfigs := tsschannel.ErrorNonJitterChannelConfigs{ + TxManager: txManager, + MaxBufferSize: cfg.ErrorHandlerServiceJitterChannelBufferSize, + MaxWorkers: cfg.ErrorHandlerServiceJitterChannelMaxWorkers, + MaxRetries: cfg.ErrorHandlerServiceJitterChannelMaxRetries, + WaitBtwnRetriesMS: cfg.ErrorHandlerServiceJitterChannelMinWaitBtwnRetriesMS, } - nonJitterChannel := tsschannel.NewErrorHandlerServiceNonJitterChannel(nonJitterChannelOpts) - - errHandlerService := tssservices.NewErrorHandlerService(tssservices.ErrorHandlerServiceConfigs{ - JitterChannel: jitterChannel, - NonJitterChannel: nonJitterChannel, - }) - - webhookHandlerService := tssservices.NewWebhookHandlerService(nil) + errorNonJitterChannel := tsschannel.NewErrorNonJitterChannel(errorNonJitterChannelConfigs) router := tssrouter.NewRouter(tssrouter.RouterConfigs{ - ErrorHandlerService: errHandlerService, - WebhookHandlerService: webhookHandlerService, + RPCCallerChannel: rpcCallerChannel, + ErrorJitterChannel: errorJitterChannel, + ErrorNonJitterChannel: errorNonJitterChannel, + WebhookChannel: nil, }) - jitterChannel.SetRouter(router) - nonJitterChannel.SetRouter(router) - rpcCallerServiceChannel.SetRouter(router) + rpcCallerChannel.SetRouter(router) + errorJitterChannel.SetRouter(router) + errorNonJitterChannel.SetRouter(router) return handlerDeps{ Models: models, @@ -264,8 +250,10 @@ func initHandlerDeps(cfg Configs) (handlerDeps, error) { PaymentService: paymentService, AppTracker: cfg.AppTracker, // TSS - RPCCallerServiceChannel: rpcCallerServiceChannel, - TSSRouter: router, + RPCCallerChannel: rpcCallerChannel, + ErrorJitterChannel: errorJitterChannel, + ErrorNonJitterChannel: errorNonJitterChannel, + TSSRouter: router, }, nil } From 22ed8011ee030ffaee56ee67af1fa7af762a5052 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Thu, 26 Sep 2024 01:32:07 -0700 Subject: [PATCH 071/113] checking error on route.Route --- internal/tss/channels/error_jitter_channel.go | 5 ++++- internal/tss/channels/error_non_jitter_channel.go | 6 +++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/internal/tss/channels/error_jitter_channel.go b/internal/tss/channels/error_jitter_channel.go index cd38fb8..52097f2 100644 --- a/internal/tss/channels/error_jitter_channel.go +++ b/internal/tss/channels/error_jitter_channel.go @@ -79,7 +79,10 @@ func (p *errorJitterPool) Receive(payload tss.Payload) { if i == p.MaxRetries { // Retry limit reached, route the payload to the router so it can re-route it to this pool and keep re-trying // NOTE: Is this a good idea? Infinite tries per transaction ? - p.Router.Route(payload) + err := p.Router.Route(payload) + if err != nil { + log.Errorf("%s: Unable to route payload: %e", ErrorJitterChannelName, err) + } } } diff --git a/internal/tss/channels/error_non_jitter_channel.go b/internal/tss/channels/error_non_jitter_channel.go index 5daab12..f82a050 100644 --- a/internal/tss/channels/error_non_jitter_channel.go +++ b/internal/tss/channels/error_non_jitter_channel.go @@ -75,7 +75,11 @@ func (p *errorNonJitterPool) Receive(payload tss.Payload) { if i == p.MaxRetries { // Retry limit reached, route the payload to the router so it can re-route it to this pool and keep re-trying // NOTE: Is this a good idea? - p.Router.Route(payload) + err := p.Router.Route(payload) + if err != nil { + log.Errorf("%s: Unable to route payload: %e", ErrorNonJitterChannelName, err) + return + } } } From 9cb0145d1b844f9ea7883d394e2f36164fa6460b Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Thu, 26 Sep 2024 01:35:02 -0700 Subject: [PATCH 072/113] check error --- internal/tss/channels/error_non_jitter_channel.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/tss/channels/error_non_jitter_channel.go b/internal/tss/channels/error_non_jitter_channel.go index f82a050..af8a4f9 100644 --- a/internal/tss/channels/error_non_jitter_channel.go +++ b/internal/tss/channels/error_non_jitter_channel.go @@ -64,7 +64,7 @@ func (p *errorNonJitterPool) Receive(payload tss.Payload) { } payload.RpcSubmitTxResponse = rpcSendResp if !slices.Contains(tss.NonJitterErrorCodes, rpcSendResp.Code.TxResultCode) { - p.Router.Route(payload) + err := p.Router.Route(payload) if err != nil { log.Errorf("%s: Unable to route payload: %e", ErrorNonJitterChannelName, err) return From 9118a06f22b66b6f011d8c31513a1e6173dd1683 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Thu, 26 Sep 2024 11:54:16 -0700 Subject: [PATCH 073/113] merging main --- internal/entities/rpc.go | 34 +++++++++---------- .../tss/services/transaction_manager_test.go | 8 ++--- internal/tss/types.go | 2 +- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/internal/entities/rpc.go b/internal/entities/rpc.go index eb252bb..cec0ecc 100644 --- a/internal/entities/rpc.go +++ b/internal/entities/rpc.go @@ -35,24 +35,24 @@ type RPCGetLedgerEntriesResult struct { } type RPCGetTransactionResult struct { - Status string `json:"status"` - LatestLedger int64 `json:"latestLedger"` - LatestLedgerCloseTime string `json:"latestLedgerCloseTime"` - OldestLedger string `json:"oldestLedger"` - OldestLedgerCloseTime string `json:"oldestLedgerCloseTime"` - ApplicationOrder string `json:"applicationOrder"` - EnvelopeXDR string `json:"envelopeXdr"` - ResultXDR string `json:"resultXdr"` - ResultMetaXDR string `json:"resultMetaXdr"` - Ledger string `json:"ledger"` - CreatedAt string `json:"createdAt"` - ErrorResultXDR string `json:"errorResultXdr"` + Status RPCStatus `json:"status"` + LatestLedger int64 `json:"latestLedger"` + LatestLedgerCloseTime string `json:"latestLedgerCloseTime"` + OldestLedger int64 `json:"oldestLedger"` + OldestLedgerCloseTime string `json:"oldestLedgerCloseTime"` + ApplicationOrder int64 `json:"applicationOrder"` + EnvelopeXDR string `json:"envelopeXdr"` + ResultXDR string `json:"resultXdr"` + ResultMetaXDR string `json:"resultMetaXdr"` + Ledger int64 `json:"ledger"` + CreatedAt string `json:"createdAt"` + ErrorResultXDR string `json:"errorResultXdr"` } type RPCSendTransactionResult struct { - Status string `json:"status"` - LatestLedger int64 `json:"latestLedger"` - LatestLedgerCloseTime string `json:"latestLedgerCloseTime"` - Hash string `json:"hash"` - ErrorResultXDR string `json:"errorResultXdr"` + Status RPCStatus `json:"status"` + LatestLedger int64 `json:"latestLedger"` + LatestLedgerCloseTime string `json:"latestLedgerCloseTime"` + Hash string `json:"hash"` + ErrorResultXDR string `json:"errorResultXdr"` } diff --git a/internal/tss/services/transaction_manager_test.go b/internal/tss/services/transaction_manager_test.go index 5ed8d22..80731d4 100644 --- a/internal/tss/services/transaction_manager_test.go +++ b/internal/tss/services/transaction_manager_test.go @@ -60,7 +60,7 @@ func TestBuildAndSubmitTransaction(t *testing.T) { t.Run("rpc_call_fail", func(t *testing.T) { _ = store.UpsertTransaction(context.Background(), payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.RPCTXStatus{OtherStatus: tss.NewStatus}) - sendResp := entities.RPCSendTransactionResult{Status: string(entities.ErrorStatus)} + sendResp := entities.RPCSendTransactionResult{Status: entities.ErrorStatus} txServiceMock. On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). @@ -92,7 +92,7 @@ func TestBuildAndSubmitTransaction(t *testing.T) { t.Run("rpc_resp_empty_errorresult_xdr", func(t *testing.T) { _ = store.UpsertTransaction(context.Background(), payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.RPCTXStatus{OtherStatus: tss.NewStatus}) sendResp := entities.RPCSendTransactionResult{ - Status: string(entities.PendingStatus), + Status: entities.PendingStatus, ErrorResultXDR: "", } @@ -127,7 +127,7 @@ func TestBuildAndSubmitTransaction(t *testing.T) { t.Run("rpc_resp_has_unparsable_errorresult_xdr", func(t *testing.T) { _ = store.UpsertTransaction(context.Background(), payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.RPCTXStatus{OtherStatus: tss.NewStatus}) sendResp := entities.RPCSendTransactionResult{ - Status: string(entities.ErrorStatus), + Status: entities.ErrorStatus, ErrorResultXDR: "ABCD", } @@ -160,7 +160,7 @@ func TestBuildAndSubmitTransaction(t *testing.T) { t.Run("rpc_returns_response", func(t *testing.T) { _ = store.UpsertTransaction(context.Background(), payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.RPCTXStatus{OtherStatus: tss.NewStatus}) sendResp := entities.RPCSendTransactionResult{ - Status: string(entities.ErrorStatus), + Status: entities.ErrorStatus, ErrorResultXDR: "AAAAAAAAAMj////9AAAAAA==", } diff --git a/internal/tss/types.go b/internal/tss/types.go index f512974..5c737f0 100644 --- a/internal/tss/types.go +++ b/internal/tss/types.go @@ -106,7 +106,7 @@ func ParseToRPCSendTxResponse(transactionXDR string, result entities.RPCSendTran sendTxResponse.Code.OtherCodes = RPCFailCode return sendTxResponse, fmt.Errorf("RPC fail: %w", err) } - sendTxResponse.Status.RPCStatus = entities.RPCStatus(result.Status) + sendTxResponse.Status.RPCStatus = result.Status sendTxResponse.TransactionHash = result.Hash sendTxResponse.Code, err = parseSendTransactionErrorXDR(result.ErrorResultXDR) if err != nil { From 12425f9b9d1e5320aa5240ec76243815b69c1e6b Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Thu, 26 Sep 2024 12:01:39 -0700 Subject: [PATCH 074/113] fixing parsesendresp tests --- internal/tss/types_test.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/tss/types_test.go b/internal/tss/types_test.go index 375a4dc..fff7637 100644 --- a/internal/tss/types_test.go +++ b/internal/tss/types_test.go @@ -25,9 +25,9 @@ func TestParseToRPCSendTxResponse(t *testing.T) { ErrorResultXDR: "", }, nil) - assert.Equal(t, entities.PendingStatus, resp.Status) - assert.Equal(t, UnmarshalBinaryCode, resp.Code.OtherCodes) - assert.Equal(t, "parse error result xdr string: unable to unmarshal errorResultXDR: ", err.Error()) + assert.Equal(t, entities.PendingStatus, resp.Status.RPCStatus) + assert.Equal(t, EmptyCode, resp.Code.OtherCodes) + assert.Empty(t, err) }) t.Run("response_has_unparsable_errorResultXdr", func(t *testing.T) { From 6e93627d3cf389e86fa912a9450c992ad53d73ed Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Thu, 26 Sep 2024 14:26:25 -0700 Subject: [PATCH 075/113] commit changes --- internal/entities/rpc.go | 58 ++ internal/services/rpc_service.go | 104 +++ internal/services/rpc_service_test.go | 257 +++++++ .../servicesmocks/rpc_service_mocks.go | 27 + ...handler_service_non_jitter_channel_test.go | 224 ------ ...ter_channel.go => error_jitter_channel.go} | 53 +- .../tss/channels/error_jitter_channel_test.go | 141 ++++ ...channel.go => error_non_jitter_channel.go} | 47 +- .../channels/error_non_jitter_channel_test.go | 140 ++++ ...ror_service_handler_jitter_channel_test.go | 224 ------ .../channels/rpc_caller_service_channel.go | 78 --- .../rpc_caller_service_channel_test.go | 207 ------ internal/tss/channels/utils.go | 54 -- ..._service_channel.go => webhook_channel.go} | 5 +- ...hannel_test.go => webhook_channel_test.go} | 5 +- internal/tss/router/mocks.go | 5 +- internal/tss/router/router.go | 79 +-- internal/tss/router/router_test.go | 130 +++- .../tss/services/error_handler_service.go | 36 - .../services/error_handler_service_test.go | 54 -- internal/tss/services/mocks.go | 43 +- internal/tss/services/rpc_caller_service.go | 21 - internal/tss/services/transaction_manager.go | 76 ++ .../transaction_manager_test.go} | 108 ++- .../transaction_service.go | 112 +-- .../tss/services/transaction_service_test.go | 236 +++++++ internal/tss/services/types.go | 7 - .../tss/services/webhook_handler_service.go | 19 - internal/tss/store/store.go | 2 +- internal/tss/store/store_test.go | 9 +- internal/tss/types.go | 174 +++-- internal/tss/types_new.go | 0 internal/tss/types_old.go | 121 ++++ internal/tss/types_test.go | 92 +++ internal/tss/utils/helpers.go | 4 +- internal/tss/utils/mocks.go | 50 -- internal/tss/utils/transaction_builder.go | 6 +- .../tss/utils/transaction_service_test.go | 648 ------------------ internal/utils/http_client.go | 21 + 39 files changed, 1744 insertions(+), 1933 deletions(-) create mode 100644 internal/entities/rpc.go create mode 100644 internal/services/rpc_service.go create mode 100644 internal/services/rpc_service_test.go create mode 100644 internal/services/servicesmocks/rpc_service_mocks.go delete mode 100644 internal/tss/channels/error_handler_service_non_jitter_channel_test.go rename internal/tss/channels/{error_handler_service_jitter_channel.go => error_jitter_channel.go} (51%) create mode 100644 internal/tss/channels/error_jitter_channel_test.go rename internal/tss/channels/{error_handler_service_non_jitter_channel.go => error_non_jitter_channel.go} (53%) create mode 100644 internal/tss/channels/error_non_jitter_channel_test.go delete mode 100644 internal/tss/channels/error_service_handler_jitter_channel_test.go delete mode 100644 internal/tss/channels/rpc_caller_service_channel.go delete mode 100644 internal/tss/channels/rpc_caller_service_channel_test.go delete mode 100644 internal/tss/channels/utils.go rename internal/tss/channels/{webhook_handler_service_channel.go => webhook_channel.go} (91%) rename internal/tss/channels/{webhook_handler_service_channel_test.go => webhook_channel_test.go} (86%) delete mode 100644 internal/tss/services/error_handler_service.go delete mode 100644 internal/tss/services/error_handler_service_test.go delete mode 100644 internal/tss/services/rpc_caller_service.go create mode 100644 internal/tss/services/transaction_manager.go rename internal/tss/{channels/utils_test.go => services/transaction_manager_test.go} (50%) rename internal/tss/{utils => services}/transaction_service.go (50%) create mode 100644 internal/tss/services/transaction_service_test.go delete mode 100644 internal/tss/services/types.go delete mode 100644 internal/tss/services/webhook_handler_service.go create mode 100644 internal/tss/types_new.go create mode 100644 internal/tss/types_old.go create mode 100644 internal/tss/types_test.go delete mode 100644 internal/tss/utils/mocks.go delete mode 100644 internal/tss/utils/transaction_service_test.go create mode 100644 internal/utils/http_client.go diff --git a/internal/entities/rpc.go b/internal/entities/rpc.go new file mode 100644 index 0000000..cec0ecc --- /dev/null +++ b/internal/entities/rpc.go @@ -0,0 +1,58 @@ +package entities + +import ( + "encoding/json" +) + +type RPCStatus string + +const ( + // sendTransaction statuses + PendingStatus RPCStatus = "PENDING" + DuplicateStatus RPCStatus = "DUPLICATE" + TryAgainLaterStatus RPCStatus = "TRY_AGAIN_LATER" + ErrorStatus RPCStatus = "ERROR" + // getTransaction statuses + NotFoundStatus RPCStatus = "NOT_FOUND" + FailedStatus RPCStatus = "FAILED" + SuccessStatus RPCStatus = "SUCCESS" +) + +type RPCEntry struct { + Key string `json:"key"` + XDR string `json:"xdr"` + LastModifiedLedgerSeq int64 `json:"lastModifiedLedgerSeq"` +} + +type RPCResponse struct { + Result json.RawMessage `json:"result"` + JSONRPC string `json:"jsonrpc"` + ID int64 `json:"id"` +} + +type RPCGetLedgerEntriesResult struct { + Entries []RPCEntry `json:"entries"` +} + +type RPCGetTransactionResult struct { + Status RPCStatus `json:"status"` + LatestLedger int64 `json:"latestLedger"` + LatestLedgerCloseTime string `json:"latestLedgerCloseTime"` + OldestLedger int64 `json:"oldestLedger"` + OldestLedgerCloseTime string `json:"oldestLedgerCloseTime"` + ApplicationOrder int64 `json:"applicationOrder"` + EnvelopeXDR string `json:"envelopeXdr"` + ResultXDR string `json:"resultXdr"` + ResultMetaXDR string `json:"resultMetaXdr"` + Ledger int64 `json:"ledger"` + CreatedAt string `json:"createdAt"` + ErrorResultXDR string `json:"errorResultXdr"` +} + +type RPCSendTransactionResult struct { + Status RPCStatus `json:"status"` + LatestLedger int64 `json:"latestLedger"` + LatestLedgerCloseTime string `json:"latestLedgerCloseTime"` + Hash string `json:"hash"` + ErrorResultXDR string `json:"errorResultXdr"` +} diff --git a/internal/services/rpc_service.go b/internal/services/rpc_service.go new file mode 100644 index 0000000..c3a0a53 --- /dev/null +++ b/internal/services/rpc_service.go @@ -0,0 +1,104 @@ +package services + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + + "github.com/stellar/wallet-backend/internal/entities" + "github.com/stellar/wallet-backend/internal/utils" +) + +type RPCService interface { + GetTransaction(transactionHash string) (entities.RPCGetTransactionResult, error) + SendTransaction(transactionXDR string) (entities.RPCSendTransactionResult, error) +} + +type rpcService struct { + rpcURL string + httpClient utils.HTTPClient +} + +var _ RPCService = (*rpcService)(nil) + +func NewRPCService(rpcURL string, httpClient utils.HTTPClient) (*rpcService, error) { + if rpcURL == "" { + return nil, errors.New("rpcURL cannot be nil") + } + if httpClient == nil { + return nil, errors.New("httpClient cannot be nil") + } + + return &rpcService{ + rpcURL: rpcURL, + httpClient: httpClient, + }, nil +} + +func (r *rpcService) GetTransaction(transactionHash string) (entities.RPCGetTransactionResult, error) { + resultBytes, err := r.sendRPCRequest("getTransaction", map[string]string{"hash": transactionHash}) + if err != nil { + return entities.RPCGetTransactionResult{}, fmt.Errorf("sending getTransaction request: %w", err) + } + + var result entities.RPCGetTransactionResult + err = json.Unmarshal(resultBytes, &result) + if err != nil { + return entities.RPCGetTransactionResult{}, fmt.Errorf("parsing getTransaction result JSON: %w", err) + } + + return result, nil +} + +func (r *rpcService) SendTransaction(transactionXDR string) (entities.RPCSendTransactionResult, error) { + resultBytes, err := r.sendRPCRequest("sendTransaction", map[string]string{"transaction": transactionXDR}) + if err != nil { + return entities.RPCSendTransactionResult{}, fmt.Errorf("sending sendTransaction request: %w", err) + } + + var result entities.RPCSendTransactionResult + err = json.Unmarshal(resultBytes, &result) + if err != nil { + return entities.RPCSendTransactionResult{}, fmt.Errorf("parsing sendTransaction result JSON: %w", err) + } + + return result, nil +} + +func (r *rpcService) sendRPCRequest(method string, params map[string]string) (json.RawMessage, error) { + payload := map[string]interface{}{ + "jsonrpc": "2.0", + "id": 1, + "method": method, + "params": params, + } + jsonData, err := json.Marshal(payload) + if err != nil { + return nil, fmt.Errorf("marshaling payload") + } + + resp, err := r.httpClient.Post(r.rpcURL, "application/json", bytes.NewBuffer(jsonData)) + if err != nil { + return nil, fmt.Errorf("sending POST request to RPC: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("unmarshaling RPC response: %w", err) + } + + var res entities.RPCResponse + err = json.Unmarshal(body, &res) + if err != nil { + return nil, fmt.Errorf("parsing RPC response JSON: %w", err) + } + + if res.Result == nil { + return nil, fmt.Errorf("response %s missing result field", string(body)) + } + + return res.Result, nil +} diff --git a/internal/services/rpc_service_test.go b/internal/services/rpc_service_test.go new file mode 100644 index 0000000..26cabb1 --- /dev/null +++ b/internal/services/rpc_service_test.go @@ -0,0 +1,257 @@ +package services + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strings" + "testing" + + "github.com/stellar/wallet-backend/internal/entities" + "github.com/stellar/wallet-backend/internal/utils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +type errorReader struct{} + +func (e *errorReader) Read(p []byte) (n int, err error) { + return 0, fmt.Errorf("read error") +} + +func (e *errorReader) Close() error { + return nil +} + +func TestSendRPCRequest(t *testing.T) { + mockHTTPClient := utils.MockHTTPClient{} + rpcURL := "http://api.vibrantapp.com/soroban/rpc" + rpcService, _ := NewRPCService(rpcURL, &mockHTTPClient) + + t.Run("successful", func(t *testing.T) { + httpResponse := http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{ + "jsonrpc": "2.0", + "id": 8675309, + "result": { + "testValue": "theTestValue" + } + }`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", mock.Anything). + Return(&httpResponse, nil). + Once() + + resp, err := rpcService.sendRPCRequest("sendTransaction", nil) + require.NoError(t, err) + + var resultStruct struct { + TestValue string `json:"testValue"` + } + err = json.Unmarshal(resp, &resultStruct) + require.NoError(t, err) + + assert.Equal(t, "theTestValue", resultStruct.TestValue) + }) + + t.Run("rpc_post_call_fails", func(t *testing.T) { + mockHTTPClient. + On("Post", rpcURL, "application/json", mock.Anything). + Return(&http.Response{}, errors.New("connection failed")). + Once() + + resp, err := rpcService.sendRPCRequest("sendTransaction", nil) + assert.Nil(t, resp) + assert.Equal(t, "sending POST request to RPC: connection failed", err.Error()) + }) + + t.Run("unmarshaling_rpc_response_fails", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(&errorReader{}), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", mock.Anything). + Return(httpResponse, nil). + Once() + + resp, err := rpcService.sendRPCRequest("sendTransaction", nil) + assert.Nil(t, resp) + assert.Equal(t, "unmarshaling RPC response: read error", err.Error()) + }) + + t.Run("unmarshaling_json_fails", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{invalid-json`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", mock.Anything). + Return(httpResponse, nil). + Once() + + resp, err := rpcService.sendRPCRequest("sendTransaction", nil) + assert.Nil(t, resp) + assert.Equal(t, "parsing RPC response JSON: invalid character 'i' looking for beginning of object key string", err.Error()) + }) + + t.Run("response_has_no_result_field", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"status": "success"}`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", mock.Anything). + Return(httpResponse, nil). + Once() + + result, err := rpcService.sendRPCRequest("sendTransaction", nil) + assert.Empty(t, result) + assert.Equal(t, `response {"status": "success"} missing result field`, err.Error()) + }) +} + +func TestSendTransaction(t *testing.T) { + mockHTTPClient := utils.MockHTTPClient{} + rpcURL := "http://api.vibrantapp.com/soroban/rpc" + rpcService, _ := NewRPCService(rpcURL, &mockHTTPClient) + + t.Run("successful", func(t *testing.T) { + transactionXDR := "AAAAAgAAAABYJgX6SmA2tGVDv3GXfOWbkeL869ahE0e5DG9HnXQw/QAAAGQAAjpnAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAACxaDFEbbssZfrbRgFxTYIygITSQxsUpDmneN2gAZBEFQAAAAAAAAAABfXhAAAAAAAAAAAA" + + payload := map[string]interface{}{ + "jsonrpc": "2.0", + "id": 1, + "method": "sendTransaction", + "params": map[string]string{ + "transaction": transactionXDR, + }, + } + jsonData, _ := json.Marshal(payload) + + httpResponse := http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{ + "jsonrpc": "2.0", + "id": 8675309, + "result": { + "status": "PENDING", + "hash": "d8ec9b68780314ffdfdfc2194b1b35dd27d7303c3bceaef6447e31631a1419dc", + "latestLedger": 2553978, + "latestLedgerCloseTime": "1700159337" + } + }`)), + } + + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(&httpResponse, nil). + Once() + + result, err := rpcService.SendTransaction(transactionXDR) + require.NoError(t, err) + + assert.Equal(t, entities.RPCSendTransactionResult{ + Status: "PENDING", + Hash: "d8ec9b68780314ffdfdfc2194b1b35dd27d7303c3bceaef6447e31631a1419dc", + LatestLedger: 2553978, + LatestLedgerCloseTime: "1700159337", + }, result) + }) + + t.Run("rpc_request_fails", func(t *testing.T) { + mockHTTPClient. + On("Post", rpcURL, "application/json", mock.Anything). + Return(&http.Response{}, errors.New("connection failed")). + Once() + + result, err := rpcService.SendTransaction("XDR") + require.Error(t, err) + + assert.Equal(t, entities.RPCSendTransactionResult{}, result) + assert.Equal(t, "sending sendTransaction request: sending POST request to RPC: connection failed", err.Error()) + }) +} + +func TestGetTransaction(t *testing.T) { + mockHTTPClient := utils.MockHTTPClient{} + rpcURL := "http://api.vibrantapp.com/soroban/rpc" + rpcService, _ := NewRPCService(rpcURL, &mockHTTPClient) + + t.Run("successful", func(t *testing.T) { + transactionHash := "6bc97bddc21811c626839baf4ab574f4f9f7ddbebb44d286ae504396d4e752da" + + payload := map[string]interface{}{ + "jsonrpc": "2.0", + "id": 1, + "method": "getTransaction", + "params": map[string]string{ + "hash": transactionHash, + }, + } + jsonData, _ := json.Marshal(payload) + + httpResponse := http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{ + "jsonrpc": "2.0", + "id": 8675309, + "result": { + "status": "SUCCESS", + "latestLedger": 2540076, + "latestLedgerCloseTime": "1700086333", + "oldestLedger": 2538637, + "oldestLedgerCloseTime": "1700078796", + "applicationOrder": 1, + "envelopeXdr": "AAAAAgAAAADGFY14/R1KD0VGtTbi5Yp4d7LuMW0iQbLM/AUiGKj5owCpsoQAJY3OAAAjqgAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAGAAAAAAAAAABhhOwI+RL18Zpk7cqI5pRRf0L96jE8i+0x3ekhuBh2cUAAAARc2V0X2N1cnJlbmN5X3JhdGUAAAAAAAACAAAADwAAAANldXIAAAAACQAAAAAAAAAAAAAAAAARCz4AAAABAAAAAAAAAAAAAAABhhOwI+RL18Zpk7cqI5pRRf0L96jE8i+0x3ekhuBh2cUAAAARc2V0X2N1cnJlbmN5X3JhdGUAAAAAAAACAAAADwAAAANldXIAAAAACQAAAAAAAAAAAAAAAAARCz4AAAAAAAAAAQAAAAAAAAABAAAAB4408vVXuLU3mry897TfPpYjjsSN7n42REos241RddYdAAAAAQAAAAYAAAABhhOwI+RL18Zpk7cqI5pRRf0L96jE8i+0x3ekhuBh2cUAAAAUAAAAAQFvcYAAAImAAAAHxAAAAAAAAAACAAAAARio+aMAAABATbFMyom/TUz87wHex0LoYZA8jbNJkXbaDSgmOdk+wSBFJuMuta+/vSlro0e0vK2+1FqD/zWHZeYig4pKmM3rDA==", + "resultXdr": "AAAAAAARFy8AAAAAAAAAAQAAAAAAAAAYAAAAAMu8SHUN67hTUJOz3q+IrH9M/4dCVXaljeK6x1Ss20YWAAAAAA==", + "resultMetaXdr": "AAAAAwAAAAAAAAACAAAAAwAmwiAAAAAAAAAAAMYVjXj9HUoPRUa1NuLlinh3su4xbSJBssz8BSIYqPmjAAAAFUHZob0AJY3OAAAjqQAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAMAAAAAACbCHwAAAABlVUH3AAAAAAAAAAEAJsIgAAAAAAAAAADGFY14/R1KD0VGtTbi5Yp4d7LuMW0iQbLM/AUiGKj5owAAABVB2aG9ACWNzgAAI6oAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAADAAAAAAAmwiAAAAAAZVVB/AAAAAAAAAABAAAAAgAAAAMAJsIfAAAABgAAAAAAAAABhhOwI+RL18Zpk7cqI5pRRf0L96jE8i+0x3ekhuBh2cUAAAAUAAAAAQAAABMAAAAAjjTy9Ve4tTeavLz3tN8+liOOxI3ufjZESizbjVF11h0AAAABAAAABQAAABAAAAABAAAAAQAAAA8AAAAJQ29yZVN0YXRlAAAAAAAAEQAAAAEAAAAGAAAADwAAAAVhZG1pbgAAAAAAABIAAAAAAAAAADn1LT+CCK/HiHMChoEi/AtPrkos4XRR2E45Pr25lb3/AAAADwAAAAljb2xfdG9rZW4AAAAAAAASAAAAAdeSi3LCcDzP6vfrn/TvTVBKVai5efybRQ6iyEK00c5hAAAADwAAAAxvcmFjbGVfYWRtaW4AAAASAAAAAAAAAADGFY14/R1KD0VGtTbi5Yp4d7LuMW0iQbLM/AUiGKj5owAAAA8AAAAKcGFuaWNfbW9kZQAAAAAAAAAAAAAAAAAPAAAAEHByb3RvY29sX21hbmFnZXIAAAASAAAAAAAAAAAtSfyAwmj05lZ0WduHsQYQZgvahCNVtZyqS2HRC99kyQAAAA8AAAANc3RhYmxlX2lzc3VlcgAAAAAAABIAAAAAAAAAAEM5BlXva0R5UN6SCMY+6evwJa4mY/f062z0TKLnqN4wAAAAEAAAAAEAAAACAAAADwAAAAhDdXJyZW5jeQAAAA8AAAADZXVyAAAAABEAAAABAAAABQAAAA8AAAAGYWN0aXZlAAAAAAAAAAAAAQAAAA8AAAAIY29udHJhY3QAAAASAAAAAUGpebFxuPbvxZFzOxh8TWAxUwFgraPxPuJEY/8yhiYEAAAADwAAAAxkZW5vbWluYXRpb24AAAAPAAAAA2V1cgAAAAAPAAAAC2xhc3RfdXBkYXRlAAAAAAUAAAAAZVVBvgAAAA8AAAAEcmF0ZQAAAAkAAAAAAAAAAAAAAAAAEQb8AAAAEAAAAAEAAAACAAAADwAAAAhDdXJyZW5jeQAAAA8AAAADdXNkAAAAABEAAAABAAAABQAAAA8AAAAGYWN0aXZlAAAAAAAAAAAAAQAAAA8AAAAIY29udHJhY3QAAAASAAAAATUEqdkvrE2LnSiwOwed3v4VEaulOEiS1rxQw6rJkfxCAAAADwAAAAxkZW5vbWluYXRpb24AAAAPAAAAA3VzZAAAAAAPAAAAC2xhc3RfdXBkYXRlAAAAAAUAAAAAZVVB9wAAAA8AAAAEcmF0ZQAAAAkAAAAAAAAAAAAAAAAAEnzuAAAAEAAAAAEAAAACAAAADwAAAApWYXVsdHNJbmZvAAAAAAAPAAAAA2V1cgAAAAARAAAAAQAAAAgAAAAPAAAADGRlbm9taW5hdGlvbgAAAA8AAAADZXVyAAAAAA8AAAAKbG93ZXN0X2tleQAAAAAAEAAAAAEAAAACAAAADwAAAARTb21lAAAAEQAAAAEAAAADAAAADwAAAAdhY2NvdW50AAAAABIAAAAAAAAAAGKaH7iFUU2kfGOJGONeYuJ2U2QUeQ+zOEfYZvAoeHDsAAAADwAAAAxkZW5vbWluYXRpb24AAAAPAAAAA2V1cgAAAAAPAAAABWluZGV4AAAAAAAACQAAAAAAAAAAAAAAA7msoAAAAAAPAAAADG1pbl9jb2xfcmF0ZQAAAAkAAAAAAAAAAAAAAAAAp9jAAAAADwAAABFtaW5fZGVidF9jcmVhdGlvbgAAAAAAAAkAAAAAAAAAAAAAAAA7msoAAAAADwAAABBvcGVuaW5nX2NvbF9yYXRlAAAACQAAAAAAAAAAAAAAAACveeAAAAAPAAAACXRvdGFsX2NvbAAAAAAAAAkAAAAAAAAAAAAAAAlQL5AAAAAADwAAAAp0b3RhbF9kZWJ0AAAAAAAJAAAAAAAAAAAAAAAAlQL5AAAAAA8AAAAMdG90YWxfdmF1bHRzAAAABQAAAAAAAAABAAAAEAAAAAEAAAACAAAADwAAAApWYXVsdHNJbmZvAAAAAAAPAAAAA3VzZAAAAAARAAAAAQAAAAgAAAAPAAAADGRlbm9taW5hdGlvbgAAAA8AAAADdXNkAAAAAA8AAAAKbG93ZXN0X2tleQAAAAAAEAAAAAEAAAACAAAADwAAAARTb21lAAAAEQAAAAEAAAADAAAADwAAAAdhY2NvdW50AAAAABIAAAAAAAAAAGKaH7iFUU2kfGOJGONeYuJ2U2QUeQ+zOEfYZvAoeHDsAAAADwAAAAxkZW5vbWluYXRpb24AAAAPAAAAA3VzZAAAAAAPAAAABWluZGV4AAAAAAAACQAAAAAAAAAAAAAAA7msoAAAAAAPAAAADG1pbl9jb2xfcmF0ZQAAAAkAAAAAAAAAAAAAAAAAp9jAAAAADwAAABFtaW5fZGVidF9jcmVhdGlvbgAAAAAAAAkAAAAAAAAAAAAAAAA7msoAAAAADwAAABBvcGVuaW5nX2NvbF9yYXRlAAAACQAAAAAAAAAAAAAAAACveeAAAAAPAAAACXRvdGFsX2NvbAAAAAAAAAkAAAAAAAAAAAAAABF2WS4AAAAADwAAAAp0b3RhbF9kZWJ0AAAAAAAJAAAAAAAAAAAAAAAA7msoAAAAAA8AAAAMdG90YWxfdmF1bHRzAAAABQAAAAAAAAACAAAAAAAAAAEAJsIgAAAABgAAAAAAAAABhhOwI+RL18Zpk7cqI5pRRf0L96jE8i+0x3ekhuBh2cUAAAAUAAAAAQAAABMAAAAAjjTy9Ve4tTeavLz3tN8+liOOxI3ufjZESizbjVF11h0AAAABAAAABQAAABAAAAABAAAAAQAAAA8AAAAJQ29yZVN0YXRlAAAAAAAAEQAAAAEAAAAGAAAADwAAAAVhZG1pbgAAAAAAABIAAAAAAAAAADn1LT+CCK/HiHMChoEi/AtPrkos4XRR2E45Pr25lb3/AAAADwAAAAljb2xfdG9rZW4AAAAAAAASAAAAAdeSi3LCcDzP6vfrn/TvTVBKVai5efybRQ6iyEK00c5hAAAADwAAAAxvcmFjbGVfYWRtaW4AAAASAAAAAAAAAADGFY14/R1KD0VGtTbi5Yp4d7LuMW0iQbLM/AUiGKj5owAAAA8AAAAKcGFuaWNfbW9kZQAAAAAAAAAAAAAAAAAPAAAAEHByb3RvY29sX21hbmFnZXIAAAASAAAAAAAAAAAtSfyAwmj05lZ0WduHsQYQZgvahCNVtZyqS2HRC99kyQAAAA8AAAANc3RhYmxlX2lzc3VlcgAAAAAAABIAAAAAAAAAAEM5BlXva0R5UN6SCMY+6evwJa4mY/f062z0TKLnqN4wAAAAEAAAAAEAAAACAAAADwAAAAhDdXJyZW5jeQAAAA8AAAADZXVyAAAAABEAAAABAAAABQAAAA8AAAAGYWN0aXZlAAAAAAAAAAAAAQAAAA8AAAAIY29udHJhY3QAAAASAAAAAUGpebFxuPbvxZFzOxh8TWAxUwFgraPxPuJEY/8yhiYEAAAADwAAAAxkZW5vbWluYXRpb24AAAAPAAAAA2V1cgAAAAAPAAAAC2xhc3RfdXBkYXRlAAAAAAUAAAAAZVVB/AAAAA8AAAAEcmF0ZQAAAAkAAAAAAAAAAAAAAAAAEQs+AAAAEAAAAAEAAAACAAAADwAAAAhDdXJyZW5jeQAAAA8AAAADdXNkAAAAABEAAAABAAAABQAAAA8AAAAGYWN0aXZlAAAAAAAAAAAAAQAAAA8AAAAIY29udHJhY3QAAAASAAAAATUEqdkvrE2LnSiwOwed3v4VEaulOEiS1rxQw6rJkfxCAAAADwAAAAxkZW5vbWluYXRpb24AAAAPAAAAA3VzZAAAAAAPAAAAC2xhc3RfdXBkYXRlAAAAAAUAAAAAZVVB9wAAAA8AAAAEcmF0ZQAAAAkAAAAAAAAAAAAAAAAAEnzuAAAAEAAAAAEAAAACAAAADwAAAApWYXVsdHNJbmZvAAAAAAAPAAAAA2V1cgAAAAARAAAAAQAAAAgAAAAPAAAADGRlbm9taW5hdGlvbgAAAA8AAAADZXVyAAAAAA8AAAAKbG93ZXN0X2tleQAAAAAAEAAAAAEAAAACAAAADwAAAARTb21lAAAAEQAAAAEAAAADAAAADwAAAAdhY2NvdW50AAAAABIAAAAAAAAAAGKaH7iFUU2kfGOJGONeYuJ2U2QUeQ+zOEfYZvAoeHDsAAAADwAAAAxkZW5vbWluYXRpb24AAAAPAAAAA2V1cgAAAAAPAAAABWluZGV4AAAAAAAACQAAAAAAAAAAAAAAA7msoAAAAAAPAAAADG1pbl9jb2xfcmF0ZQAAAAkAAAAAAAAAAAAAAAAAp9jAAAAADwAAABFtaW5fZGVidF9jcmVhdGlvbgAAAAAAAAkAAAAAAAAAAAAAAAA7msoAAAAADwAAABBvcGVuaW5nX2NvbF9yYXRlAAAACQAAAAAAAAAAAAAAAACveeAAAAAPAAAACXRvdGFsX2NvbAAAAAAAAAkAAAAAAAAAAAAAAAlQL5AAAAAADwAAAAp0b3RhbF9kZWJ0AAAAAAAJAAAAAAAAAAAAAAAAlQL5AAAAAA8AAAAMdG90YWxfdmF1bHRzAAAABQAAAAAAAAABAAAAEAAAAAEAAAACAAAADwAAAApWYXVsdHNJbmZvAAAAAAAPAAAAA3VzZAAAAAARAAAAAQAAAAgAAAAPAAAADGRlbm9taW5hdGlvbgAAAA8AAAADdXNkAAAAAA8AAAAKbG93ZXN0X2tleQAAAAAAEAAAAAEAAAACAAAADwAAAARTb21lAAAAEQAAAAEAAAADAAAADwAAAAdhY2NvdW50AAAAABIAAAAAAAAAAGKaH7iFUU2kfGOJGONeYuJ2U2QUeQ+zOEfYZvAoeHDsAAAADwAAAAxkZW5vbWluYXRpb24AAAAPAAAAA3VzZAAAAAAPAAAABWluZGV4AAAAAAAACQAAAAAAAAAAAAAAA7msoAAAAAAPAAAADG1pbl9jb2xfcmF0ZQAAAAkAAAAAAAAAAAAAAAAAp9jAAAAADwAAABFtaW5fZGVidF9jcmVhdGlvbgAAAAAAAAkAAAAAAAAAAAAAAAA7msoAAAAADwAAABBvcGVuaW5nX2NvbF9yYXRlAAAACQAAAAAAAAAAAAAAAACveeAAAAAPAAAACXRvdGFsX2NvbAAAAAAAAAkAAAAAAAAAAAAAABF2WS4AAAAADwAAAAp0b3RhbF9kZWJ0AAAAAAAJAAAAAAAAAAAAAAAA7msoAAAAAA8AAAAMdG90YWxfdmF1bHRzAAAABQAAAAAAAAACAAAAAAAAAAAAAAABAAAAAAAAAAAAAAABAAAAFQAAAAEAAAAAAAAAAAAAAAIAAAAAAAAAAwAAAA8AAAAHZm5fY2FsbAAAAAANAAAAIIYTsCPkS9fGaZO3KiOaUUX9C/eoxPIvtMd3pIbgYdnFAAAADwAAABFzZXRfY3VycmVuY3lfcmF0ZQAAAAAAABAAAAABAAAAAgAAAA8AAAADZXVyAAAAAAkAAAAAAAAAAAAAAAAAEQs+AAAAAQAAAAAAAAABhhOwI+RL18Zpk7cqI5pRRf0L96jE8i+0x3ekhuBh2cUAAAACAAAAAAAAAAIAAAAPAAAACWZuX3JldHVybgAAAAAAAA8AAAARc2V0X2N1cnJlbmN5X3JhdGUAAAAAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAADwAAAAxjb3JlX21ldHJpY3MAAAAPAAAACnJlYWRfZW50cnkAAAAAAAUAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAA8AAAAMY29yZV9tZXRyaWNzAAAADwAAAAt3cml0ZV9lbnRyeQAAAAAFAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAPAAAADGNvcmVfbWV0cmljcwAAAA8AAAAQbGVkZ2VyX3JlYWRfYnl0ZQAAAAUAAAAAAACJaAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAA8AAAAMY29yZV9tZXRyaWNzAAAADwAAABFsZWRnZXJfd3JpdGVfYnl0ZQAAAAAAAAUAAAAAAAAHxAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAA8AAAAMY29yZV9tZXRyaWNzAAAADwAAAA1yZWFkX2tleV9ieXRlAAAAAAAABQAAAAAAAABUAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAADwAAAAxjb3JlX21ldHJpY3MAAAAPAAAADndyaXRlX2tleV9ieXRlAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAPAAAADGNvcmVfbWV0cmljcwAAAA8AAAAOcmVhZF9kYXRhX2J5dGUAAAAAAAUAAAAAAAAH6AAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAA8AAAAMY29yZV9tZXRyaWNzAAAADwAAAA93cml0ZV9kYXRhX2J5dGUAAAAABQAAAAAAAAfEAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAADwAAAAxjb3JlX21ldHJpY3MAAAAPAAAADnJlYWRfY29kZV9ieXRlAAAAAAAFAAAAAAAAgYAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAPAAAADGNvcmVfbWV0cmljcwAAAA8AAAAPd3JpdGVfY29kZV9ieXRlAAAAAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAA8AAAAMY29yZV9tZXRyaWNzAAAADwAAAAplbWl0X2V2ZW50AAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAPAAAADGNvcmVfbWV0cmljcwAAAA8AAAAPZW1pdF9ldmVudF9ieXRlAAAAAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAA8AAAAMY29yZV9tZXRyaWNzAAAADwAAAAhjcHVfaW5zbgAAAAUAAAAAATLTQAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAA8AAAAMY29yZV9tZXRyaWNzAAAADwAAAAhtZW1fYnl0ZQAAAAUAAAAAACqhewAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAA8AAAAMY29yZV9tZXRyaWNzAAAADwAAABFpbnZva2VfdGltZV9uc2VjcwAAAAAAAAUAAAAAABFfSQAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAA8AAAAMY29yZV9tZXRyaWNzAAAADwAAAA9tYXhfcndfa2V5X2J5dGUAAAAABQAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAADwAAAAxjb3JlX21ldHJpY3MAAAAPAAAAEG1heF9yd19kYXRhX2J5dGUAAAAFAAAAAAAAB+gAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAPAAAADGNvcmVfbWV0cmljcwAAAA8AAAAQbWF4X3J3X2NvZGVfYnl0ZQAAAAUAAAAAAACBgAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAA8AAAAMY29yZV9tZXRyaWNzAAAADwAAABNtYXhfZW1pdF9ldmVudF9ieXRlAAAAAAUAAAAAAAAAAA==", + "ledger": 2540064, + "createdAt": "1700086268" + } + }`)), + } + + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(&httpResponse, nil). + Once() + + result, err := rpcService.GetTransaction(transactionHash) + require.NoError(t, err) + + assert.Equal(t, entities.RPCGetTransactionResult{ + Status: "SUCCESS", + LatestLedger: 2540076, + LatestLedgerCloseTime: "1700086333", + OldestLedger: 2538637, + OldestLedgerCloseTime: "1700078796", + ApplicationOrder: 1, + EnvelopeXDR: "AAAAAgAAAADGFY14/R1KD0VGtTbi5Yp4d7LuMW0iQbLM/AUiGKj5owCpsoQAJY3OAAAjqgAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAGAAAAAAAAAABhhOwI+RL18Zpk7cqI5pRRf0L96jE8i+0x3ekhuBh2cUAAAARc2V0X2N1cnJlbmN5X3JhdGUAAAAAAAACAAAADwAAAANldXIAAAAACQAAAAAAAAAAAAAAAAARCz4AAAABAAAAAAAAAAAAAAABhhOwI+RL18Zpk7cqI5pRRf0L96jE8i+0x3ekhuBh2cUAAAARc2V0X2N1cnJlbmN5X3JhdGUAAAAAAAACAAAADwAAAANldXIAAAAACQAAAAAAAAAAAAAAAAARCz4AAAAAAAAAAQAAAAAAAAABAAAAB4408vVXuLU3mry897TfPpYjjsSN7n42REos241RddYdAAAAAQAAAAYAAAABhhOwI+RL18Zpk7cqI5pRRf0L96jE8i+0x3ekhuBh2cUAAAAUAAAAAQFvcYAAAImAAAAHxAAAAAAAAAACAAAAARio+aMAAABATbFMyom/TUz87wHex0LoYZA8jbNJkXbaDSgmOdk+wSBFJuMuta+/vSlro0e0vK2+1FqD/zWHZeYig4pKmM3rDA==", + ResultXDR: "AAAAAAARFy8AAAAAAAAAAQAAAAAAAAAYAAAAAMu8SHUN67hTUJOz3q+IrH9M/4dCVXaljeK6x1Ss20YWAAAAAA==", + ResultMetaXDR: "AAAAAwAAAAAAAAACAAAAAwAmwiAAAAAAAAAAAMYVjXj9HUoPRUa1NuLlinh3su4xbSJBssz8BSIYqPmjAAAAFUHZob0AJY3OAAAjqQAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAMAAAAAACbCHwAAAABlVUH3AAAAAAAAAAEAJsIgAAAAAAAAAADGFY14/R1KD0VGtTbi5Yp4d7LuMW0iQbLM/AUiGKj5owAAABVB2aG9ACWNzgAAI6oAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAADAAAAAAAmwiAAAAAAZVVB/AAAAAAAAAABAAAAAgAAAAMAJsIfAAAABgAAAAAAAAABhhOwI+RL18Zpk7cqI5pRRf0L96jE8i+0x3ekhuBh2cUAAAAUAAAAAQAAABMAAAAAjjTy9Ve4tTeavLz3tN8+liOOxI3ufjZESizbjVF11h0AAAABAAAABQAAABAAAAABAAAAAQAAAA8AAAAJQ29yZVN0YXRlAAAAAAAAEQAAAAEAAAAGAAAADwAAAAVhZG1pbgAAAAAAABIAAAAAAAAAADn1LT+CCK/HiHMChoEi/AtPrkos4XRR2E45Pr25lb3/AAAADwAAAAljb2xfdG9rZW4AAAAAAAASAAAAAdeSi3LCcDzP6vfrn/TvTVBKVai5efybRQ6iyEK00c5hAAAADwAAAAxvcmFjbGVfYWRtaW4AAAASAAAAAAAAAADGFY14/R1KD0VGtTbi5Yp4d7LuMW0iQbLM/AUiGKj5owAAAA8AAAAKcGFuaWNfbW9kZQAAAAAAAAAAAAAAAAAPAAAAEHByb3RvY29sX21hbmFnZXIAAAASAAAAAAAAAAAtSfyAwmj05lZ0WduHsQYQZgvahCNVtZyqS2HRC99kyQAAAA8AAAANc3RhYmxlX2lzc3VlcgAAAAAAABIAAAAAAAAAAEM5BlXva0R5UN6SCMY+6evwJa4mY/f062z0TKLnqN4wAAAAEAAAAAEAAAACAAAADwAAAAhDdXJyZW5jeQAAAA8AAAADZXVyAAAAABEAAAABAAAABQAAAA8AAAAGYWN0aXZlAAAAAAAAAAAAAQAAAA8AAAAIY29udHJhY3QAAAASAAAAAUGpebFxuPbvxZFzOxh8TWAxUwFgraPxPuJEY/8yhiYEAAAADwAAAAxkZW5vbWluYXRpb24AAAAPAAAAA2V1cgAAAAAPAAAAC2xhc3RfdXBkYXRlAAAAAAUAAAAAZVVBvgAAAA8AAAAEcmF0ZQAAAAkAAAAAAAAAAAAAAAAAEQb8AAAAEAAAAAEAAAACAAAADwAAAAhDdXJyZW5jeQAAAA8AAAADdXNkAAAAABEAAAABAAAABQAAAA8AAAAGYWN0aXZlAAAAAAAAAAAAAQAAAA8AAAAIY29udHJhY3QAAAASAAAAATUEqdkvrE2LnSiwOwed3v4VEaulOEiS1rxQw6rJkfxCAAAADwAAAAxkZW5vbWluYXRpb24AAAAPAAAAA3VzZAAAAAAPAAAAC2xhc3RfdXBkYXRlAAAAAAUAAAAAZVVB9wAAAA8AAAAEcmF0ZQAAAAkAAAAAAAAAAAAAAAAAEnzuAAAAEAAAAAEAAAACAAAADwAAAApWYXVsdHNJbmZvAAAAAAAPAAAAA2V1cgAAAAARAAAAAQAAAAgAAAAPAAAADGRlbm9taW5hdGlvbgAAAA8AAAADZXVyAAAAAA8AAAAKbG93ZXN0X2tleQAAAAAAEAAAAAEAAAACAAAADwAAAARTb21lAAAAEQAAAAEAAAADAAAADwAAAAdhY2NvdW50AAAAABIAAAAAAAAAAGKaH7iFUU2kfGOJGONeYuJ2U2QUeQ+zOEfYZvAoeHDsAAAADwAAAAxkZW5vbWluYXRpb24AAAAPAAAAA2V1cgAAAAAPAAAABWluZGV4AAAAAAAACQAAAAAAAAAAAAAAA7msoAAAAAAPAAAADG1pbl9jb2xfcmF0ZQAAAAkAAAAAAAAAAAAAAAAAp9jAAAAADwAAABFtaW5fZGVidF9jcmVhdGlvbgAAAAAAAAkAAAAAAAAAAAAAAAA7msoAAAAADwAAABBvcGVuaW5nX2NvbF9yYXRlAAAACQAAAAAAAAAAAAAAAACveeAAAAAPAAAACXRvdGFsX2NvbAAAAAAAAAkAAAAAAAAAAAAAAAlQL5AAAAAADwAAAAp0b3RhbF9kZWJ0AAAAAAAJAAAAAAAAAAAAAAAAlQL5AAAAAA8AAAAMdG90YWxfdmF1bHRzAAAABQAAAAAAAAABAAAAEAAAAAEAAAACAAAADwAAAApWYXVsdHNJbmZvAAAAAAAPAAAAA3VzZAAAAAARAAAAAQAAAAgAAAAPAAAADGRlbm9taW5hdGlvbgAAAA8AAAADdXNkAAAAAA8AAAAKbG93ZXN0X2tleQAAAAAAEAAAAAEAAAACAAAADwAAAARTb21lAAAAEQAAAAEAAAADAAAADwAAAAdhY2NvdW50AAAAABIAAAAAAAAAAGKaH7iFUU2kfGOJGONeYuJ2U2QUeQ+zOEfYZvAoeHDsAAAADwAAAAxkZW5vbWluYXRpb24AAAAPAAAAA3VzZAAAAAAPAAAABWluZGV4AAAAAAAACQAAAAAAAAAAAAAAA7msoAAAAAAPAAAADG1pbl9jb2xfcmF0ZQAAAAkAAAAAAAAAAAAAAAAAp9jAAAAADwAAABFtaW5fZGVidF9jcmVhdGlvbgAAAAAAAAkAAAAAAAAAAAAAAAA7msoAAAAADwAAABBvcGVuaW5nX2NvbF9yYXRlAAAACQAAAAAAAAAAAAAAAACveeAAAAAPAAAACXRvdGFsX2NvbAAAAAAAAAkAAAAAAAAAAAAAABF2WS4AAAAADwAAAAp0b3RhbF9kZWJ0AAAAAAAJAAAAAAAAAAAAAAAA7msoAAAAAA8AAAAMdG90YWxfdmF1bHRzAAAABQAAAAAAAAACAAAAAAAAAAEAJsIgAAAABgAAAAAAAAABhhOwI+RL18Zpk7cqI5pRRf0L96jE8i+0x3ekhuBh2cUAAAAUAAAAAQAAABMAAAAAjjTy9Ve4tTeavLz3tN8+liOOxI3ufjZESizbjVF11h0AAAABAAAABQAAABAAAAABAAAAAQAAAA8AAAAJQ29yZVN0YXRlAAAAAAAAEQAAAAEAAAAGAAAADwAAAAVhZG1pbgAAAAAAABIAAAAAAAAAADn1LT+CCK/HiHMChoEi/AtPrkos4XRR2E45Pr25lb3/AAAADwAAAAljb2xfdG9rZW4AAAAAAAASAAAAAdeSi3LCcDzP6vfrn/TvTVBKVai5efybRQ6iyEK00c5hAAAADwAAAAxvcmFjbGVfYWRtaW4AAAASAAAAAAAAAADGFY14/R1KD0VGtTbi5Yp4d7LuMW0iQbLM/AUiGKj5owAAAA8AAAAKcGFuaWNfbW9kZQAAAAAAAAAAAAAAAAAPAAAAEHByb3RvY29sX21hbmFnZXIAAAASAAAAAAAAAAAtSfyAwmj05lZ0WduHsQYQZgvahCNVtZyqS2HRC99kyQAAAA8AAAANc3RhYmxlX2lzc3VlcgAAAAAAABIAAAAAAAAAAEM5BlXva0R5UN6SCMY+6evwJa4mY/f062z0TKLnqN4wAAAAEAAAAAEAAAACAAAADwAAAAhDdXJyZW5jeQAAAA8AAAADZXVyAAAAABEAAAABAAAABQAAAA8AAAAGYWN0aXZlAAAAAAAAAAAAAQAAAA8AAAAIY29udHJhY3QAAAASAAAAAUGpebFxuPbvxZFzOxh8TWAxUwFgraPxPuJEY/8yhiYEAAAADwAAAAxkZW5vbWluYXRpb24AAAAPAAAAA2V1cgAAAAAPAAAAC2xhc3RfdXBkYXRlAAAAAAUAAAAAZVVB/AAAAA8AAAAEcmF0ZQAAAAkAAAAAAAAAAAAAAAAAEQs+AAAAEAAAAAEAAAACAAAADwAAAAhDdXJyZW5jeQAAAA8AAAADdXNkAAAAABEAAAABAAAABQAAAA8AAAAGYWN0aXZlAAAAAAAAAAAAAQAAAA8AAAAIY29udHJhY3QAAAASAAAAATUEqdkvrE2LnSiwOwed3v4VEaulOEiS1rxQw6rJkfxCAAAADwAAAAxkZW5vbWluYXRpb24AAAAPAAAAA3VzZAAAAAAPAAAAC2xhc3RfdXBkYXRlAAAAAAUAAAAAZVVB9wAAAA8AAAAEcmF0ZQAAAAkAAAAAAAAAAAAAAAAAEnzuAAAAEAAAAAEAAAACAAAADwAAAApWYXVsdHNJbmZvAAAAAAAPAAAAA2V1cgAAAAARAAAAAQAAAAgAAAAPAAAADGRlbm9taW5hdGlvbgAAAA8AAAADZXVyAAAAAA8AAAAKbG93ZXN0X2tleQAAAAAAEAAAAAEAAAACAAAADwAAAARTb21lAAAAEQAAAAEAAAADAAAADwAAAAdhY2NvdW50AAAAABIAAAAAAAAAAGKaH7iFUU2kfGOJGONeYuJ2U2QUeQ+zOEfYZvAoeHDsAAAADwAAAAxkZW5vbWluYXRpb24AAAAPAAAAA2V1cgAAAAAPAAAABWluZGV4AAAAAAAACQAAAAAAAAAAAAAAA7msoAAAAAAPAAAADG1pbl9jb2xfcmF0ZQAAAAkAAAAAAAAAAAAAAAAAp9jAAAAADwAAABFtaW5fZGVidF9jcmVhdGlvbgAAAAAAAAkAAAAAAAAAAAAAAAA7msoAAAAADwAAABBvcGVuaW5nX2NvbF9yYXRlAAAACQAAAAAAAAAAAAAAAACveeAAAAAPAAAACXRvdGFsX2NvbAAAAAAAAAkAAAAAAAAAAAAAAAlQL5AAAAAADwAAAAp0b3RhbF9kZWJ0AAAAAAAJAAAAAAAAAAAAAAAAlQL5AAAAAA8AAAAMdG90YWxfdmF1bHRzAAAABQAAAAAAAAABAAAAEAAAAAEAAAACAAAADwAAAApWYXVsdHNJbmZvAAAAAAAPAAAAA3VzZAAAAAARAAAAAQAAAAgAAAAPAAAADGRlbm9taW5hdGlvbgAAAA8AAAADdXNkAAAAAA8AAAAKbG93ZXN0X2tleQAAAAAAEAAAAAEAAAACAAAADwAAAARTb21lAAAAEQAAAAEAAAADAAAADwAAAAdhY2NvdW50AAAAABIAAAAAAAAAAGKaH7iFUU2kfGOJGONeYuJ2U2QUeQ+zOEfYZvAoeHDsAAAADwAAAAxkZW5vbWluYXRpb24AAAAPAAAAA3VzZAAAAAAPAAAABWluZGV4AAAAAAAACQAAAAAAAAAAAAAAA7msoAAAAAAPAAAADG1pbl9jb2xfcmF0ZQAAAAkAAAAAAAAAAAAAAAAAp9jAAAAADwAAABFtaW5fZGVidF9jcmVhdGlvbgAAAAAAAAkAAAAAAAAAAAAAAAA7msoAAAAADwAAABBvcGVuaW5nX2NvbF9yYXRlAAAACQAAAAAAAAAAAAAAAACveeAAAAAPAAAACXRvdGFsX2NvbAAAAAAAAAkAAAAAAAAAAAAAABF2WS4AAAAADwAAAAp0b3RhbF9kZWJ0AAAAAAAJAAAAAAAAAAAAAAAA7msoAAAAAA8AAAAMdG90YWxfdmF1bHRzAAAABQAAAAAAAAACAAAAAAAAAAAAAAABAAAAAAAAAAAAAAABAAAAFQAAAAEAAAAAAAAAAAAAAAIAAAAAAAAAAwAAAA8AAAAHZm5fY2FsbAAAAAANAAAAIIYTsCPkS9fGaZO3KiOaUUX9C/eoxPIvtMd3pIbgYdnFAAAADwAAABFzZXRfY3VycmVuY3lfcmF0ZQAAAAAAABAAAAABAAAAAgAAAA8AAAADZXVyAAAAAAkAAAAAAAAAAAAAAAAAEQs+AAAAAQAAAAAAAAABhhOwI+RL18Zpk7cqI5pRRf0L96jE8i+0x3ekhuBh2cUAAAACAAAAAAAAAAIAAAAPAAAACWZuX3JldHVybgAAAAAAAA8AAAARc2V0X2N1cnJlbmN5X3JhdGUAAAAAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAADwAAAAxjb3JlX21ldHJpY3MAAAAPAAAACnJlYWRfZW50cnkAAAAAAAUAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAA8AAAAMY29yZV9tZXRyaWNzAAAADwAAAAt3cml0ZV9lbnRyeQAAAAAFAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAPAAAADGNvcmVfbWV0cmljcwAAAA8AAAAQbGVkZ2VyX3JlYWRfYnl0ZQAAAAUAAAAAAACJaAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAA8AAAAMY29yZV9tZXRyaWNzAAAADwAAABFsZWRnZXJfd3JpdGVfYnl0ZQAAAAAAAAUAAAAAAAAHxAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAA8AAAAMY29yZV9tZXRyaWNzAAAADwAAAA1yZWFkX2tleV9ieXRlAAAAAAAABQAAAAAAAABUAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAADwAAAAxjb3JlX21ldHJpY3MAAAAPAAAADndyaXRlX2tleV9ieXRlAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAPAAAADGNvcmVfbWV0cmljcwAAAA8AAAAOcmVhZF9kYXRhX2J5dGUAAAAAAAUAAAAAAAAH6AAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAA8AAAAMY29yZV9tZXRyaWNzAAAADwAAAA93cml0ZV9kYXRhX2J5dGUAAAAABQAAAAAAAAfEAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAADwAAAAxjb3JlX21ldHJpY3MAAAAPAAAADnJlYWRfY29kZV9ieXRlAAAAAAAFAAAAAAAAgYAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAPAAAADGNvcmVfbWV0cmljcwAAAA8AAAAPd3JpdGVfY29kZV9ieXRlAAAAAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAA8AAAAMY29yZV9tZXRyaWNzAAAADwAAAAplbWl0X2V2ZW50AAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAPAAAADGNvcmVfbWV0cmljcwAAAA8AAAAPZW1pdF9ldmVudF9ieXRlAAAAAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAA8AAAAMY29yZV9tZXRyaWNzAAAADwAAAAhjcHVfaW5zbgAAAAUAAAAAATLTQAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAA8AAAAMY29yZV9tZXRyaWNzAAAADwAAAAhtZW1fYnl0ZQAAAAUAAAAAACqhewAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAA8AAAAMY29yZV9tZXRyaWNzAAAADwAAABFpbnZva2VfdGltZV9uc2VjcwAAAAAAAAUAAAAAABFfSQAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAA8AAAAMY29yZV9tZXRyaWNzAAAADwAAAA9tYXhfcndfa2V5X2J5dGUAAAAABQAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAADwAAAAxjb3JlX21ldHJpY3MAAAAPAAAAEG1heF9yd19kYXRhX2J5dGUAAAAFAAAAAAAAB+gAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAPAAAADGNvcmVfbWV0cmljcwAAAA8AAAAQbWF4X3J3X2NvZGVfYnl0ZQAAAAUAAAAAAACBgAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAA8AAAAMY29yZV9tZXRyaWNzAAAADwAAABNtYXhfZW1pdF9ldmVudF9ieXRlAAAAAAUAAAAAAAAAAA==", + Ledger: 2540064, + CreatedAt: "1700086268", + ErrorResultXDR: "", + }, result) + }) + + t.Run("rpc_request_fails", func(t *testing.T) { + mockHTTPClient. + On("Post", rpcURL, "application/json", mock.Anything). + Return(&http.Response{}, errors.New("connection failed")). + Once() + + result, err := rpcService.GetTransaction("hash") + require.Error(t, err) + + assert.Equal(t, entities.RPCGetTransactionResult{}, result) + assert.Equal(t, "sending getTransaction request: sending POST request to RPC: connection failed", err.Error()) + }) +} diff --git a/internal/services/servicesmocks/rpc_service_mocks.go b/internal/services/servicesmocks/rpc_service_mocks.go new file mode 100644 index 0000000..f72c348 --- /dev/null +++ b/internal/services/servicesmocks/rpc_service_mocks.go @@ -0,0 +1,27 @@ +package servicesmocks + +import ( + "github.com/stellar/wallet-backend/internal/entities" + "github.com/stellar/wallet-backend/internal/services" + "github.com/stretchr/testify/mock" +) + +type RPCServiceMock struct { + mock.Mock +} + +var _ services.RPCService = (*RPCServiceMock)(nil) + +func (r *RPCServiceMock) SendTransaction(transactionXdr string) (entities.RPCSendTransactionResult, error) { + args := r.Called(transactionXdr) + return args.Get(0).(entities.RPCSendTransactionResult), args.Error(1) +} + +func (r *RPCServiceMock) GetTransaction(transactionHash string) (entities.RPCGetTransactionResult, error) { + args := r.Called(transactionHash) + return args.Get(0).(entities.RPCGetTransactionResult), args.Error(1) +} + +type TransactionManagerMock struct { + mock.Mock +} diff --git a/internal/tss/channels/error_handler_service_non_jitter_channel_test.go b/internal/tss/channels/error_handler_service_non_jitter_channel_test.go deleted file mode 100644 index 51eae3d..0000000 --- a/internal/tss/channels/error_handler_service_non_jitter_channel_test.go +++ /dev/null @@ -1,224 +0,0 @@ -package channels - -import ( - "context" - "errors" - "testing" - - "github.com/stellar/go/xdr" - "github.com/stellar/wallet-backend/internal/db" - "github.com/stellar/wallet-backend/internal/db/dbtest" - "github.com/stellar/wallet-backend/internal/tss" - "github.com/stellar/wallet-backend/internal/tss/router" - "github.com/stellar/wallet-backend/internal/tss/store" - "github.com/stellar/wallet-backend/internal/tss/utils" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" -) - -func TestNonJitterSend(t *testing.T) { - dbt := dbtest.Open(t) - defer dbt.Close() - - dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) - require.NoError(t, err) - defer dbConnectionPool.Close() - store := store.NewStore(dbConnectionPool) - txServiceMock := utils.TransactionServiceMock{} - cfg := RPCErrorHandlerServiceNonJitterChannelConfigs{ - Store: store, - TxService: &txServiceMock, - MaxBufferSize: 1, - MaxWorkers: 1, - MaxRetries: 3, - WaitBtwnRetriesMS: 10, - } - channel := NewErrorHandlerServiceNonJitterChannel(cfg) - - payload := tss.Payload{} - payload.WebhookURL = "www.stellar.com" - payload.TransactionHash = "hash" - payload.TransactionXDR = "xdr" - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(nil, errors.New("signing failed")) - - _ = store.UpsertTransaction(context.Background(), payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.NewStatus) - - channel.Send(payload) - channel.Stop() - - var status string - err = dbConnectionPool.GetContext(context.Background(), &status, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, status, string(tss.NewStatus)) -} - -func TestNonJitterReceive(t *testing.T) { - dbt := dbtest.Open(t) - defer dbt.Close() - - dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) - require.NoError(t, err) - defer dbConnectionPool.Close() - store := store.NewStore(dbConnectionPool) - txServiceMock := utils.TransactionServiceMock{} - cfg := RPCErrorHandlerServiceNonJitterChannelConfigs{ - Store: store, - TxService: &txServiceMock, - MaxBufferSize: 1, - MaxWorkers: 1, - MaxRetries: 3, - WaitBtwnRetriesMS: 10, - } - channel := NewErrorHandlerServiceNonJitterChannel(cfg) - - mockRouter := router.MockRouter{} - defer mockRouter.AssertExpectations(t) - channel.SetRouter(&mockRouter) - networkPass := "passphrase" - feeBumpTx := utils.BuildTestFeeBumpTransaction() - feeBumpTxXDR, _ := feeBumpTx.Base64() - feeBumpTxHash, _ := feeBumpTx.HashHex(networkPass) - payload := tss.Payload{} - payload.WebhookURL = "www.stellar.com" - payload.TransactionHash = "hash" - payload.TransactionXDR = "xdr" - - _ = store.UpsertTransaction(context.Background(), payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.NewStatus) - - t.Run("signing_and_submitting_tx_fails", func(t *testing.T) { - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(nil, errors.New("sign tx failed")). - Once() - - channel.Receive(payload) - - var txStatus string - err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, string(tss.NewStatus), txStatus) - - }) - t.Run("payload_gets_routed", func(t *testing.T) { - sendResp := tss.RPCSendTxResponse{} - sendResp.Status = tss.TryAgainLaterStatus - sendResp.TransactionHash = feeBumpTxHash - sendResp.TransactionXDR = feeBumpTxXDR - sendResp.Code.TxResultCode = xdr.TransactionResultCodeTxInsufficientFee - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(feeBumpTx, nil). - Once(). - On("NetworkPassphrase"). - Return(networkPass). - Once(). - On("SendTransaction", feeBumpTxXDR). - Return(sendResp, nil). - Once() - - mockRouter. - On("Route", mock.AnythingOfType("tss.Payload")). - Return(). - Once() - - channel.Receive(payload) - - var txStatus string - err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, string(tss.TryAgainLaterStatus), txStatus) - - var tryStatus int - err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) - require.NoError(t, err) - assert.Equal(t, int(xdr.TransactionResultCodeTxInsufficientFee), tryStatus) - }) - - t.Run("retries", func(t *testing.T) { - sendResp1 := tss.RPCSendTxResponse{} - sendResp1.Status = tss.ErrorStatus - sendResp1.TransactionHash = feeBumpTxHash - sendResp1.TransactionXDR = feeBumpTxXDR - sendResp1.Code.TxResultCode = xdr.TransactionResultCodeTxTooEarly - - sendResp2 := tss.RPCSendTxResponse{} - sendResp2.Status = tss.TryAgainLaterStatus - sendResp2.TransactionHash = feeBumpTxHash - sendResp2.TransactionXDR = feeBumpTxXDR - sendResp2.Code.TxResultCode = xdr.TransactionResultCodeTxInsufficientFee - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(feeBumpTx, nil). - Twice(). - On("NetworkPassphrase"). - Return(networkPass). - Twice() - - txServiceMock. - On("SendTransaction", feeBumpTxXDR). - Return(sendResp1, nil). - Once() - - txServiceMock. - On("SendTransaction", feeBumpTxXDR). - Return(sendResp2, nil). - Once() - - mockRouter. - On("Route", mock.AnythingOfType("tss.Payload")). - Return(). - Once() - - channel.Receive(payload) - - var txStatus string - err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, string(tss.TryAgainLaterStatus), txStatus) - - var tryStatus int - err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) - require.NoError(t, err) - assert.Equal(t, int(xdr.TransactionResultCodeTxInsufficientFee), tryStatus) - }) - - t.Run("max_retries", func(t *testing.T) { - sendResp := tss.RPCSendTxResponse{} - sendResp.Status = tss.ErrorStatus - sendResp.TransactionHash = feeBumpTxHash - sendResp.TransactionXDR = feeBumpTxXDR - sendResp.Code.TxResultCode = xdr.TransactionResultCodeTxTooEarly - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(feeBumpTx, nil). - Times(3). - On("NetworkPassphrase"). - Return(networkPass). - Times(3) - - txServiceMock. - On("SendTransaction", feeBumpTxXDR). - Return(sendResp, nil). - Times(3) - - mockRouter. - On("Route", mock.AnythingOfType("tss.Payload")). - Return(). - Once() - - channel.Receive(payload) - - var txStatus string - err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, string(tss.ErrorStatus), txStatus) - - var tryStatus int - err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) - require.NoError(t, err) - assert.Equal(t, int(xdr.TransactionResultCodeTxTooEarly), tryStatus) - }) -} diff --git a/internal/tss/channels/error_handler_service_jitter_channel.go b/internal/tss/channels/error_jitter_channel.go similarity index 51% rename from internal/tss/channels/error_handler_service_jitter_channel.go rename to internal/tss/channels/error_jitter_channel.go index 931372c..52097f2 100644 --- a/internal/tss/channels/error_handler_service_jitter_channel.go +++ b/internal/tss/channels/error_jitter_channel.go @@ -9,13 +9,12 @@ import ( "github.com/stellar/go/support/log" "github.com/stellar/wallet-backend/internal/tss" "github.com/stellar/wallet-backend/internal/tss/router" - tss_store "github.com/stellar/wallet-backend/internal/tss/store" - "github.com/stellar/wallet-backend/internal/tss/utils" + "github.com/stellar/wallet-backend/internal/tss/services" + "golang.org/x/exp/rand" ) -type RPCErrorHandlerServiceJitterChannelConfigs struct { - Store tss_store.Store - TxService utils.TransactionService +type ErrorJitterChannelConfigs struct { + TxManager services.TransactionManager Router router.Router MaxBufferSize int MaxWorkers int @@ -23,60 +22,74 @@ type RPCErrorHandlerServiceJitterChannelConfigs struct { MinWaitBtwnRetriesMS int } -type rpcErrorHandlerServiceJitterPool struct { +type errorJitterPool struct { Pool *pond.WorkerPool - TxService utils.TransactionService - Store tss_store.Store + TxManager services.TransactionManager Router router.Router MaxRetries int MinWaitBtwnRetriesMS int } -func NewErrorHandlerServiceJitterChannel(cfg RPCErrorHandlerServiceJitterChannelConfigs) *rpcErrorHandlerServiceJitterPool { +var ErrorJitterChannelName = "ErrorJitterChannel" + +func jitter(dur time.Duration) time.Duration { + halfDur := int64(dur / 2) + delta := rand.Int63n(halfDur) - halfDur/2 + return dur + time.Duration(delta) +} + +func NewErrorJitterChannel(cfg ErrorJitterChannelConfigs) *errorJitterPool { pool := pond.New(cfg.MaxBufferSize, cfg.MaxWorkers, pond.Strategy(pond.Balanced())) - return &rpcErrorHandlerServiceJitterPool{ + return &errorJitterPool{ Pool: pool, - TxService: cfg.TxService, - Store: cfg.Store, + TxManager: cfg.TxManager, + Router: cfg.Router, MaxRetries: cfg.MaxRetries, MinWaitBtwnRetriesMS: cfg.MinWaitBtwnRetriesMS, } } -func (p *rpcErrorHandlerServiceJitterPool) Send(payload tss.Payload) { +func (p *errorJitterPool) Send(payload tss.Payload) { p.Pool.Submit(func() { p.Receive(payload) }) } -func (p *rpcErrorHandlerServiceJitterPool) Receive(payload tss.Payload) { +func (p *errorJitterPool) Receive(payload tss.Payload) { ctx := context.Background() var i int for i = 0; i < p.MaxRetries; i++ { currentBackoff := p.MinWaitBtwnRetriesMS * (1 << i) time.Sleep(jitter(time.Duration(currentBackoff)) * time.Microsecond) - rpcSendResp, err := BuildAndSubmitTransaction(ctx, "ErrorHandlerServiceJitterChannel", payload, p.Store, p.TxService) + rpcSendResp, err := p.TxManager.BuildAndSubmitTransaction(ctx, ErrorJitterChannelName, payload) if err != nil { - log.Errorf(err.Error()) + log.Errorf("%s: Unable to sign and submit transaction: %e", ErrorJitterChannelName, err) return } payload.RpcSubmitTxResponse = rpcSendResp if !slices.Contains(tss.JitterErrorCodes, rpcSendResp.Code.TxResultCode) { - p.Router.Route(payload) + err = p.Router.Route(payload) + if err != nil { + log.Errorf("%s: Unable to route payload: %e", ErrorJitterChannelName, err) + return + } return } } if i == p.MaxRetries { // Retry limit reached, route the payload to the router so it can re-route it to this pool and keep re-trying // NOTE: Is this a good idea? Infinite tries per transaction ? - p.Router.Route(payload) + err := p.Router.Route(payload) + if err != nil { + log.Errorf("%s: Unable to route payload: %e", ErrorJitterChannelName, err) + } } } -func (p *rpcErrorHandlerServiceJitterPool) SetRouter(router router.Router) { +func (p *errorJitterPool) SetRouter(router router.Router) { p.Router = router } -func (p *rpcErrorHandlerServiceJitterPool) Stop() { +func (p *errorJitterPool) Stop() { p.Pool.StopAndWait() } diff --git a/internal/tss/channels/error_jitter_channel_test.go b/internal/tss/channels/error_jitter_channel_test.go new file mode 100644 index 0000000..8dd746d --- /dev/null +++ b/internal/tss/channels/error_jitter_channel_test.go @@ -0,0 +1,141 @@ +package channels + +import ( + "context" + "errors" + "testing" + + "github.com/stellar/wallet-backend/internal/db" + "github.com/stellar/wallet-backend/internal/db/dbtest" + "github.com/stellar/wallet-backend/internal/entities" + "github.com/stellar/wallet-backend/internal/tss" + "github.com/stellar/wallet-backend/internal/tss/router" + "github.com/stellar/wallet-backend/internal/tss/services" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestJitterSend(t *testing.T) { + dbt := dbtest.Open(t) + defer dbt.Close() + dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) + require.NoError(t, err) + defer dbConnectionPool.Close() + txManagerMock := services.TransactionManagerMock{} + routerMock := router.MockRouter{} + cfg := ErrorJitterChannelConfigs{ + TxManager: &txManagerMock, + Router: &routerMock, + MaxBufferSize: 1, + MaxWorkers: 1, + MaxRetries: 3, + MinWaitBtwnRetriesMS: 10, + } + + channel := NewErrorJitterChannel(cfg) + + payload := tss.Payload{} + payload.WebhookURL = "www.stellar.com" + payload.TransactionHash = "hash" + payload.TransactionXDR = "xdr" + + rpcResp := tss.RPCSendTxResponse{ + Status: tss.RPCTXStatus{RPCStatus: entities.ErrorStatus}, + Code: tss.RPCTXCode{TxResultCode: tss.NonJitterErrorCodes[0]}, + } + payload.RpcSubmitTxResponse = rpcResp + + txManagerMock. + On("BuildAndSubmitTransaction", context.Background(), ErrorJitterChannelName, payload). + Return(rpcResp, nil). + Once() + + routerMock. + On("Route", payload). + Return(nil). + Once() + + channel.Send(payload) + channel.Stop() + + routerMock.AssertCalled(t, "Route", payload) +} + +func TestJitterReceive(t *testing.T) { + dbt := dbtest.Open(t) + defer dbt.Close() + dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) + require.NoError(t, err) + defer dbConnectionPool.Close() + txManagerMock := services.TransactionManagerMock{} + routerMock := router.MockRouter{} + cfg := ErrorJitterChannelConfigs{ + TxManager: &txManagerMock, + Router: &routerMock, + MaxBufferSize: 1, + MaxWorkers: 1, + MaxRetries: 3, + MinWaitBtwnRetriesMS: 10, + } + + channel := NewErrorJitterChannel(cfg) + + payload := tss.Payload{} + payload.WebhookURL = "www.stellar.com" + payload.TransactionHash = "hash" + payload.TransactionXDR = "xdr" + + t.Run("build_and_submit_tx_fail", func(t *testing.T) { + txManagerMock. + On("BuildAndSubmitTransaction", context.Background(), ErrorJitterChannelName, payload). + Return(tss.RPCSendTxResponse{}, errors.New("build tx failed")). + Once() + + channel.Receive(payload) + + routerMock.AssertNotCalled(t, "Route", payload) + }) + t.Run("retries", func(t *testing.T) { + sendResp1 := tss.RPCSendTxResponse{ + Status: tss.RPCTXStatus{RPCStatus: entities.ErrorStatus}, + Code: tss.RPCTXCode{TxResultCode: tss.JitterErrorCodes[0]}, + } + sendResp2 := tss.RPCSendTxResponse{ + Status: tss.RPCTXStatus{RPCStatus: entities.ErrorStatus}, + Code: tss.RPCTXCode{TxResultCode: tss.NonJitterErrorCodes[0]}, + } + + txManagerMock. + On("BuildAndSubmitTransaction", context.Background(), ErrorJitterChannelName, mock.AnythingOfType("tss.Payload")). + Return(sendResp1, nil). + Once() + txManagerMock. + On("BuildAndSubmitTransaction", context.Background(), ErrorJitterChannelName, mock.AnythingOfType("tss.Payload")). + Return(sendResp2, nil). + Once() + routerMock. + On("Route", mock.AnythingOfType("tss.Payload")). + Return(nil). + Once() + + channel.Receive(payload) + }) + + t.Run("max_retries", func(t *testing.T) { + sendResp := tss.RPCSendTxResponse{ + Status: tss.RPCTXStatus{RPCStatus: entities.ErrorStatus}, + Code: tss.RPCTXCode{TxResultCode: tss.JitterErrorCodes[0]}, + } + + txManagerMock. + On("BuildAndSubmitTransaction", context.Background(), ErrorJitterChannelName, mock.AnythingOfType("tss.Payload")). + Return(sendResp, nil). + Times(3) + routerMock. + On("Route", mock.AnythingOfType("tss.Payload")). + Return(nil). + Once() + + channel.Receive(payload) + }) +} diff --git a/internal/tss/channels/error_handler_service_non_jitter_channel.go b/internal/tss/channels/error_non_jitter_channel.go similarity index 53% rename from internal/tss/channels/error_handler_service_non_jitter_channel.go rename to internal/tss/channels/error_non_jitter_channel.go index 16508b0..af8a4f9 100644 --- a/internal/tss/channels/error_handler_service_non_jitter_channel.go +++ b/internal/tss/channels/error_non_jitter_channel.go @@ -2,6 +2,7 @@ package channels import ( "context" + "fmt" "slices" "time" @@ -9,13 +10,12 @@ import ( "github.com/stellar/go/support/log" "github.com/stellar/wallet-backend/internal/tss" "github.com/stellar/wallet-backend/internal/tss/router" + "github.com/stellar/wallet-backend/internal/tss/services" tss_store "github.com/stellar/wallet-backend/internal/tss/store" - "github.com/stellar/wallet-backend/internal/tss/utils" ) -type RPCErrorHandlerServiceNonJitterChannelConfigs struct { - Store tss_store.Store - TxService utils.TransactionService +type ErrorNonJitterChannelConfigs struct { + TxManager services.TransactionManager Router router.Router MaxBufferSize int MaxWorkers int @@ -23,59 +23,70 @@ type RPCErrorHandlerServiceNonJitterChannelConfigs struct { WaitBtwnRetriesMS int } -type rpcErrorHandlerServiceNonJitterPool struct { +type errorNonJitterPool struct { Pool *pond.WorkerPool - TxService utils.TransactionService + TxManager services.TransactionManager Store tss_store.Store Router router.Router MaxRetries int WaitBtwnRetriesMS int } -func NewErrorHandlerServiceNonJitterChannel(cfg RPCErrorHandlerServiceNonJitterChannelConfigs) *rpcErrorHandlerServiceNonJitterPool { +var ErrorNonJitterChannelName = "ErrorNonJitterChannel" + +func NewErrorNonJitterChannel(cfg ErrorNonJitterChannelConfigs) *errorNonJitterPool { pool := pond.New(cfg.MaxBufferSize, cfg.MaxWorkers, pond.Strategy(pond.Balanced())) - return &rpcErrorHandlerServiceNonJitterPool{ + return &errorNonJitterPool{ Pool: pool, - TxService: cfg.TxService, - Store: cfg.Store, + TxManager: cfg.TxManager, + Router: cfg.Router, MaxRetries: cfg.MaxRetries, WaitBtwnRetriesMS: cfg.WaitBtwnRetriesMS, } } -func (p *rpcErrorHandlerServiceNonJitterPool) Send(payload tss.Payload) { +func (p *errorNonJitterPool) Send(payload tss.Payload) { p.Pool.Submit(func() { p.Receive(payload) }) } -func (p *rpcErrorHandlerServiceNonJitterPool) Receive(payload tss.Payload) { +func (p *errorNonJitterPool) Receive(payload tss.Payload) { ctx := context.Background() var i int for i = 0; i < p.MaxRetries; i++ { + fmt.Println(i) time.Sleep(time.Duration(p.WaitBtwnRetriesMS) * time.Microsecond) - rpcSendResp, err := BuildAndSubmitTransaction(ctx, "ErrorHandlerServiceNonJitterChannel", payload, p.Store, p.TxService) + rpcSendResp, err := p.TxManager.BuildAndSubmitTransaction(ctx, ErrorNonJitterChannelName, payload) if err != nil { - log.Errorf(err.Error()) + log.Errorf("%s: Unable to sign and submit transaction: %e", ErrorNonJitterChannelName, err) return } payload.RpcSubmitTxResponse = rpcSendResp if !slices.Contains(tss.NonJitterErrorCodes, rpcSendResp.Code.TxResultCode) { - p.Router.Route(payload) + err := p.Router.Route(payload) + if err != nil { + log.Errorf("%s: Unable to route payload: %e", ErrorNonJitterChannelName, err) + return + } return } } if i == p.MaxRetries { // Retry limit reached, route the payload to the router so it can re-route it to this pool and keep re-trying // NOTE: Is this a good idea? - p.Router.Route(payload) + err := p.Router.Route(payload) + if err != nil { + log.Errorf("%s: Unable to route payload: %e", ErrorNonJitterChannelName, err) + return + } } } -func (p *rpcErrorHandlerServiceNonJitterPool) SetRouter(router router.Router) { +func (p *errorNonJitterPool) SetRouter(router router.Router) { p.Router = router } -func (p *rpcErrorHandlerServiceNonJitterPool) Stop() { +func (p *errorNonJitterPool) Stop() { p.Pool.StopAndWait() } diff --git a/internal/tss/channels/error_non_jitter_channel_test.go b/internal/tss/channels/error_non_jitter_channel_test.go new file mode 100644 index 0000000..94fef17 --- /dev/null +++ b/internal/tss/channels/error_non_jitter_channel_test.go @@ -0,0 +1,140 @@ +package channels + +import ( + "context" + "errors" + "testing" + + "github.com/stellar/wallet-backend/internal/db" + "github.com/stellar/wallet-backend/internal/db/dbtest" + "github.com/stellar/wallet-backend/internal/entities" + "github.com/stellar/wallet-backend/internal/tss" + "github.com/stellar/wallet-backend/internal/tss/router" + "github.com/stellar/wallet-backend/internal/tss/services" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestNonJitterSend(t *testing.T) { + dbt := dbtest.Open(t) + defer dbt.Close() + dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) + require.NoError(t, err) + defer dbConnectionPool.Close() + txManagerMock := services.TransactionManagerMock{} + routerMock := router.MockRouter{} + cfg := ErrorNonJitterChannelConfigs{ + TxManager: &txManagerMock, + Router: &routerMock, + MaxBufferSize: 1, + MaxWorkers: 1, + MaxRetries: 3, + WaitBtwnRetriesMS: 10, + } + + channel := NewErrorNonJitterChannel(cfg) + + payload := tss.Payload{} + payload.WebhookURL = "www.stellar.com" + payload.TransactionHash = "hash" + payload.TransactionXDR = "xdr" + + rpcResp := tss.RPCSendTxResponse{ + Status: tss.RPCTXStatus{RPCStatus: entities.ErrorStatus}, + Code: tss.RPCTXCode{TxResultCode: tss.JitterErrorCodes[0]}, + } + payload.RpcSubmitTxResponse = rpcResp + + txManagerMock. + On("BuildAndSubmitTransaction", context.Background(), ErrorNonJitterChannelName, payload). + Return(rpcResp, nil). + Once() + + routerMock. + On("Route", payload). + Return(nil). + Once() + + channel.Send(payload) + channel.Stop() + + routerMock.AssertCalled(t, "Route", payload) +} + +func TestNonJitterReceive(t *testing.T) { + dbt := dbtest.Open(t) + defer dbt.Close() + dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) + require.NoError(t, err) + defer dbConnectionPool.Close() + txManagerMock := services.TransactionManagerMock{} + routerMock := router.MockRouter{} + cfg := ErrorNonJitterChannelConfigs{ + TxManager: &txManagerMock, + Router: &routerMock, + MaxBufferSize: 1, + MaxWorkers: 1, + MaxRetries: 3, + WaitBtwnRetriesMS: 10, + } + + channel := NewErrorNonJitterChannel(cfg) + payload := tss.Payload{} + payload.WebhookURL = "www.stellar.com" + payload.TransactionHash = "hash" + payload.TransactionXDR = "xdr" + t.Run("build_and_submit_tx_fail", func(t *testing.T) { + txManagerMock. + On("BuildAndSubmitTransaction", context.Background(), ErrorNonJitterChannelName, payload). + Return(tss.RPCSendTxResponse{}, errors.New("build tx failed")). + Once() + + channel.Receive(payload) + + routerMock.AssertNotCalled(t, "Route", payload) + }) + + t.Run("retries", func(t *testing.T) { + sendResp1 := tss.RPCSendTxResponse{ + Status: tss.RPCTXStatus{RPCStatus: entities.ErrorStatus}, + Code: tss.RPCTXCode{TxResultCode: tss.NonJitterErrorCodes[0]}, + } + sendResp2 := tss.RPCSendTxResponse{ + Status: tss.RPCTXStatus{RPCStatus: entities.ErrorStatus}, + Code: tss.RPCTXCode{TxResultCode: tss.JitterErrorCodes[0]}, + } + + txManagerMock. + On("BuildAndSubmitTransaction", context.Background(), ErrorNonJitterChannelName, mock.AnythingOfType("tss.Payload")). + Return(sendResp1, nil). + Once() + txManagerMock. + On("BuildAndSubmitTransaction", context.Background(), ErrorNonJitterChannelName, mock.AnythingOfType("tss.Payload")). + Return(sendResp2, nil). + Once() + routerMock. + On("Route", mock.AnythingOfType("tss.Payload")). + Return(nil). + Once() + + channel.Receive(payload) + }) + + t.Run("max_retries", func(t *testing.T) { + sendResp := tss.RPCSendTxResponse{ + Status: tss.RPCTXStatus{RPCStatus: entities.ErrorStatus}, + Code: tss.RPCTXCode{TxResultCode: tss.NonJitterErrorCodes[0]}, + } + + txManagerMock. + On("BuildAndSubmitTransaction", context.Background(), ErrorNonJitterChannelName, mock.AnythingOfType("tss.Payload")). + Return(sendResp, nil). + Times(3) + routerMock. + On("Route", mock.AnythingOfType("tss.Payload")). + Return(nil). + Once() + + channel.Receive(payload) + }) +} diff --git a/internal/tss/channels/error_service_handler_jitter_channel_test.go b/internal/tss/channels/error_service_handler_jitter_channel_test.go deleted file mode 100644 index bb4077d..0000000 --- a/internal/tss/channels/error_service_handler_jitter_channel_test.go +++ /dev/null @@ -1,224 +0,0 @@ -package channels - -import ( - "context" - "errors" - "testing" - - "github.com/stellar/go/xdr" - "github.com/stellar/wallet-backend/internal/db" - "github.com/stellar/wallet-backend/internal/db/dbtest" - "github.com/stellar/wallet-backend/internal/tss" - "github.com/stellar/wallet-backend/internal/tss/router" - "github.com/stellar/wallet-backend/internal/tss/store" - "github.com/stellar/wallet-backend/internal/tss/utils" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" -) - -func TestJitterSend(t *testing.T) { - dbt := dbtest.Open(t) - defer dbt.Close() - - dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) - require.NoError(t, err) - defer dbConnectionPool.Close() - store := store.NewStore(dbConnectionPool) - txServiceMock := utils.TransactionServiceMock{} - cfg := RPCErrorHandlerServiceJitterChannelConfigs{ - Store: store, - TxService: &txServiceMock, - MaxBufferSize: 1, - MaxWorkers: 1, - MaxRetries: 3, - MinWaitBtwnRetriesMS: 10, - } - channel := NewErrorHandlerServiceJitterChannel(cfg) - - payload := tss.Payload{} - payload.WebhookURL = "www.stellar.com" - payload.TransactionHash = "hash" - payload.TransactionXDR = "xdr" - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(nil, errors.New("signing failed")) - - _ = store.UpsertTransaction(context.Background(), payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.NewStatus) - - channel.Send(payload) - channel.Stop() - - var status string - err = dbConnectionPool.GetContext(context.Background(), &status, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, status, string(tss.NewStatus)) -} - -func TestJitterReceive(t *testing.T) { - dbt := dbtest.Open(t) - defer dbt.Close() - - dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) - require.NoError(t, err) - defer dbConnectionPool.Close() - store := store.NewStore(dbConnectionPool) - txServiceMock := utils.TransactionServiceMock{} - cfg := RPCErrorHandlerServiceJitterChannelConfigs{ - Store: store, - TxService: &txServiceMock, - MaxBufferSize: 1, - MaxWorkers: 1, - MaxRetries: 3, - MinWaitBtwnRetriesMS: 10, - } - channel := NewErrorHandlerServiceJitterChannel(cfg) - - mockRouter := router.MockRouter{} - defer mockRouter.AssertExpectations(t) - channel.SetRouter(&mockRouter) - networkPass := "passphrase" - feeBumpTx := utils.BuildTestFeeBumpTransaction() - feeBumpTxXDR, _ := feeBumpTx.Base64() - feeBumpTxHash, _ := feeBumpTx.HashHex(networkPass) - payload := tss.Payload{} - payload.WebhookURL = "www.stellar.com" - payload.TransactionHash = "hash" - payload.TransactionXDR = "xdr" - - _ = store.UpsertTransaction(context.Background(), payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.NewStatus) - - t.Run("signing_and_submitting_tx_fails", func(t *testing.T) { - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(nil, errors.New("sign tx failed")). - Once() - - channel.Receive(payload) - - var txStatus string - err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, string(tss.NewStatus), txStatus) - }) - - t.Run("payload_gets_routed", func(t *testing.T) { - sendResp := tss.RPCSendTxResponse{} - sendResp.Status = tss.ErrorStatus - sendResp.TransactionHash = feeBumpTxHash - sendResp.TransactionXDR = feeBumpTxXDR - sendResp.Code.TxResultCode = xdr.TransactionResultCodeTxTooEarly - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(feeBumpTx, nil). - Once(). - On("NetworkPassphrase"). - Return(networkPass). - Once(). - On("SendTransaction", feeBumpTxXDR). - Return(sendResp, nil). - Once() - - mockRouter. - On("Route", mock.AnythingOfType("tss.Payload")). - Return(). - Once() - - channel.Receive(payload) - - var txStatus string - err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, string(tss.ErrorStatus), txStatus) - - var tryStatus int - err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) - require.NoError(t, err) - assert.Equal(t, int(xdr.TransactionResultCodeTxTooEarly), tryStatus) - }) - - t.Run("retries", func(t *testing.T) { - sendResp1 := tss.RPCSendTxResponse{} - sendResp1.Status = tss.ErrorStatus - sendResp1.TransactionHash = feeBumpTxHash - sendResp1.TransactionXDR = feeBumpTxXDR - sendResp1.Code.TxResultCode = xdr.TransactionResultCodeTxInsufficientFee - - sendResp2 := tss.RPCSendTxResponse{} - sendResp2.Status = tss.FailedStatus - sendResp2.TransactionHash = feeBumpTxHash - sendResp2.TransactionXDR = feeBumpTxXDR - sendResp2.Code.TxResultCode = xdr.TransactionResultCodeTxFailed - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(feeBumpTx, nil). - Twice(). - On("NetworkPassphrase"). - Return(networkPass). - Twice() - - txServiceMock. - On("SendTransaction", feeBumpTxXDR). - Return(sendResp1, nil). - Once() - - txServiceMock. - On("SendTransaction", feeBumpTxXDR). - Return(sendResp2, nil). - Once() - - mockRouter. - On("Route", mock.AnythingOfType("tss.Payload")). - Return(). - Once() - - channel.Receive(payload) - - var txStatus string - err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, string(tss.FailedStatus), txStatus) - - var tryStatus int - err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) - require.NoError(t, err) - assert.Equal(t, int(xdr.TransactionResultCodeTxFailed), tryStatus) - }) - - t.Run("max_retries", func(t *testing.T) { - sendResp := tss.RPCSendTxResponse{} - sendResp.Status = tss.ErrorStatus - sendResp.TransactionHash = feeBumpTxHash - sendResp.TransactionXDR = feeBumpTxXDR - sendResp.Code.TxResultCode = xdr.TransactionResultCodeTxInsufficientFee - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(feeBumpTx, nil). - Times(3). - On("NetworkPassphrase"). - Return(networkPass). - Times(3) - - txServiceMock. - On("SendTransaction", feeBumpTxXDR). - Return(sendResp, nil). - Times(3) - - mockRouter. - On("Route", mock.AnythingOfType("tss.Payload")). - Return(). - Once() - - channel.Receive(payload) - - var txStatus string - err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, string(tss.ErrorStatus), txStatus) - - var tryStatus int - err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) - require.NoError(t, err) - assert.Equal(t, int(xdr.TransactionResultCodeTxInsufficientFee), tryStatus) - }) -} diff --git a/internal/tss/channels/rpc_caller_service_channel.go b/internal/tss/channels/rpc_caller_service_channel.go deleted file mode 100644 index 33300c3..0000000 --- a/internal/tss/channels/rpc_caller_service_channel.go +++ /dev/null @@ -1,78 +0,0 @@ -package channels - -import ( - "context" - - "github.com/alitto/pond" - - "github.com/stellar/go/support/log" - "github.com/stellar/wallet-backend/internal/tss" - "github.com/stellar/wallet-backend/internal/tss/router" - "github.com/stellar/wallet-backend/internal/tss/services" - "github.com/stellar/wallet-backend/internal/tss/store" - "github.com/stellar/wallet-backend/internal/tss/utils" -) - -type RPCCallerServiceChannelConfigs struct { - Store store.Store - TxService utils.TransactionService - Router router.Router - MaxBufferSize int - MaxWorkers int -} - -type rpcCallerServicePool struct { - Pool *pond.WorkerPool - TxService utils.TransactionService - ErrHandlerService services.Service - Store store.Store - Router router.Router -} - -func NewRPCCallerServiceChannel(cfg RPCCallerServiceChannelConfigs) *rpcCallerServicePool { - pool := pond.New(cfg.MaxBufferSize, cfg.MaxWorkers, pond.Strategy(pond.Balanced())) - return &rpcCallerServicePool{ - Pool: pool, - TxService: cfg.TxService, - Store: cfg.Store, - Router: cfg.Router, - } - -} - -func (p *rpcCallerServicePool) Send(payload tss.Payload) { - p.Pool.Submit(func() { - p.Receive(payload) - }) -} - -func (p *rpcCallerServicePool) Receive(payload tss.Payload) { - - ctx := context.Background() - // Create a new transaction record in the transactions table. - err := p.Store.UpsertTransaction(ctx, payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.NewStatus) - - if err != nil { - log.Errorf("Unable to upsert transaction into transactions table: %s", err.Error()) - return - } - - rpcSendResp, err := BuildAndSubmitTransaction(ctx, "RPCCallerServiceChannel", payload, p.Store, p.TxService) - - if err != nil { - log.Errorf(": Unable to sign and submit transaction: %s", err.Error()) - return - } - payload.RpcSubmitTxResponse = rpcSendResp - if rpcSendResp.Status == tss.TryAgainLaterStatus || rpcSendResp.Status == tss.ErrorStatus { - p.Router.Route(payload) - } -} - -func (p *rpcCallerServicePool) SetRouter(router router.Router) { - p.Router = router -} - -func (p *rpcCallerServicePool) Stop() { - p.Pool.StopAndWait() -} diff --git a/internal/tss/channels/rpc_caller_service_channel_test.go b/internal/tss/channels/rpc_caller_service_channel_test.go deleted file mode 100644 index 797b56d..0000000 --- a/internal/tss/channels/rpc_caller_service_channel_test.go +++ /dev/null @@ -1,207 +0,0 @@ -package channels - -import ( - "context" - "errors" - "testing" - - "github.com/stellar/go/xdr" - "github.com/stellar/wallet-backend/internal/db" - "github.com/stellar/wallet-backend/internal/db/dbtest" - "github.com/stellar/wallet-backend/internal/tss" - "github.com/stellar/wallet-backend/internal/tss/router" - "github.com/stellar/wallet-backend/internal/tss/store" - "github.com/stellar/wallet-backend/internal/tss/utils" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" -) - -func TestSend(t *testing.T) { - dbt := dbtest.Open(t) - defer dbt.Close() - dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) - require.NoError(t, err) - defer dbConnectionPool.Close() - store := store.NewStore(dbConnectionPool) - txServiceMock := utils.TransactionServiceMock{} - cfgs := RPCCallerServiceChannelConfigs{ - Store: store, - TxService: &txServiceMock, - MaxBufferSize: 10, - MaxWorkers: 10, - } - channel := NewRPCCallerServiceChannel(cfgs) - payload := tss.Payload{} - payload.WebhookURL = "www.stellar.com" - payload.TransactionHash = "hash" - payload.TransactionXDR = "xdr" - networkPass := "passphrase" - - feeBumpTx := utils.BuildTestFeeBumpTransaction() - feeBumpTxXDR, _ := feeBumpTx.Base64() - sendResp := tss.RPCSendTxResponse{} - sendResp.Code.OtherCodes = tss.RPCFailCode - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(feeBumpTx, nil). - Once(). - On("NetworkPassphrase"). - Return(networkPass). - Once(). - On("SendTransaction", feeBumpTxXDR). - Return(sendResp, errors.New("RPC Fail")). - Once() - - channel.Send(payload) - channel.Stop() - - var status string - err = dbConnectionPool.GetContext(context.Background(), &status, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, status, string(tss.NewStatus)) - - var tryStatus int - feeBumpTxHash, _ := feeBumpTx.HashHex(networkPass) - err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) - require.NoError(t, err) - assert.Equal(t, int(tss.RPCFailCode), tryStatus) -} - -func TestReceive(t *testing.T) { - dbt := dbtest.Open(t) - defer dbt.Close() - - dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) - require.NoError(t, err) - defer dbConnectionPool.Close() - store := store.NewStore(dbConnectionPool) - txServiceMock := utils.TransactionServiceMock{} - defer txServiceMock.AssertExpectations(t) - routerMock := router.MockRouter{} - defer routerMock.AssertExpectations(t) - cfgs := RPCCallerServiceChannelConfigs{ - Store: store, - TxService: &txServiceMock, - Router: &routerMock, - MaxBufferSize: 1, - MaxWorkers: 1, - } - networkPass := "passphrase" - channel := NewRPCCallerServiceChannel(cfgs) - feeBumpTx := utils.BuildTestFeeBumpTransaction() - feeBumpTxXDR, _ := feeBumpTx.Base64() - feeBumpTxHash, _ := feeBumpTx.HashHex(networkPass) - payload := tss.Payload{} - payload.WebhookURL = "www.stellar.com" - payload.TransactionHash = "hash" - payload.TransactionXDR = "xdr" - - t.Run("fail_on_tx_build_and_sign", func(t *testing.T) { - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(nil, errors.New("signing failed")). - Once() - channel.Receive(payload) - - var status string - err = dbConnectionPool.GetContext(context.Background(), &status, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, string(tss.NewStatus), status) - }) - - t.Run("sign_and_submit_tx_fails", func(t *testing.T) { - sendResp := tss.RPCSendTxResponse{} - sendResp.Code.OtherCodes = tss.RPCFailCode - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(feeBumpTx, nil). - Once(). - On("NetworkPassphrase"). - Return(networkPass). - Once(). - On("SendTransaction", feeBumpTxXDR). - Return(sendResp, errors.New("RPC Fail")). - Once() - - channel.Receive(payload) - - var txStatus string - err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, txStatus, string(tss.NewStatus)) - - var tryStatus int - feeBumpTxHash, _ := feeBumpTx.HashHex(networkPass) - err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) - require.NoError(t, err) - assert.Equal(t, int(tss.RPCFailCode), tryStatus) - - }) - - t.Run("routes_payload", func(t *testing.T) { - sendResp := tss.RPCSendTxResponse{} - sendResp.Status = tss.ErrorStatus - sendResp.TransactionHash = feeBumpTxHash - sendResp.TransactionXDR = feeBumpTxXDR - sendResp.Code.TxResultCode = xdr.TransactionResultCodeTxTooEarly - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(feeBumpTx, nil). - Once(). - On("NetworkPassphrase"). - Return(networkPass). - Once(). - On("SendTransaction", feeBumpTxXDR). - Return(sendResp, nil). - Once() - routerMock. - On("Route", mock.AnythingOfType("tss.Payload")). - Return(). - Once() - - channel.Receive(payload) - - var txStatus string - err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, string(tss.ErrorStatus), txStatus) - - var tryStatus int - err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) - require.NoError(t, err) - assert.Equal(t, int(xdr.TransactionResultCodeTxTooEarly), tryStatus) - }) - - t.Run("does_not_routes_payload", func(t *testing.T) { - sendResp := tss.RPCSendTxResponse{} - sendResp.Status = tss.PendingStatus - sendResp.TransactionHash = feeBumpTxHash - sendResp.TransactionXDR = feeBumpTxXDR - sendResp.Code.TxResultCode = xdr.TransactionResultCodeTxSuccess - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(feeBumpTx, nil). - Once(). - On("NetworkPassphrase"). - Return(networkPass). - Once(). - On("SendTransaction", feeBumpTxXDR). - Return(sendResp, nil). - Once() - // this time the router mock is not called - - channel.Receive(payload) - - var txStatus string - err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, string(tss.PendingStatus), txStatus) - - var tryStatus int - err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) - require.NoError(t, err) - assert.Equal(t, int(xdr.TransactionResultCodeTxSuccess), tryStatus) - }) - -} diff --git a/internal/tss/channels/utils.go b/internal/tss/channels/utils.go deleted file mode 100644 index 3da7d9a..0000000 --- a/internal/tss/channels/utils.go +++ /dev/null @@ -1,54 +0,0 @@ -package channels - -import ( - "fmt" - "time" - - "github.com/stellar/wallet-backend/internal/tss" - "github.com/stellar/wallet-backend/internal/tss/store" - "github.com/stellar/wallet-backend/internal/tss/utils" - "golang.org/x/exp/rand" - "golang.org/x/net/context" -) - -func jitter(dur time.Duration) time.Duration { - halfDur := int64(dur / 2) - delta := rand.Int63n(halfDur) - halfDur/2 - return dur + time.Duration(delta) -} - -func BuildAndSubmitTransaction(ctx context.Context, channelName string, payload tss.Payload, store store.Store, txService utils.TransactionService) (tss.RPCSendTxResponse, error) { - feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(ctx, payload.TransactionXDR) - if err != nil { - return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to sign/build transaction: %s", channelName, err.Error()) - } - feeBumpTxHash, err := feeBumpTx.HashHex(txService.NetworkPassphrase()) - if err != nil { - return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to hashhex fee bump transaction: %s", channelName, err.Error()) - } - - feeBumpTxXDR, err := feeBumpTx.Base64() - if err != nil { - return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to base64 fee bump transaction: %s", channelName, err.Error()) - } - - err = store.UpsertTry(ctx, payload.TransactionHash, feeBumpTxHash, feeBumpTxXDR, tss.RPCTXCode{OtherCodes: tss.NewCode}) - if err != nil { - return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to upsert try in tries table: %s", channelName, err.Error()) - } - rpcSendResp, rpcErr := txService.SendTransaction(feeBumpTxXDR) - - err = store.UpsertTry(ctx, payload.TransactionHash, feeBumpTxHash, feeBumpTxXDR, rpcSendResp.Code) - if err != nil { - return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to upsert try in tries table: %s", channelName, err.Error()) - } - if rpcErr != nil && rpcSendResp.Code.OtherCodes == tss.RPCFailCode || rpcSendResp.Code.OtherCodes == tss.UnMarshalBinaryCode { - return tss.RPCSendTxResponse{}, fmt.Errorf("%s: RPC fail: %s", channelName, rpcErr.Error()) - } - - err = store.UpsertTransaction(ctx, payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, rpcSendResp.Status) - if err != nil { - return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to do the final update of tx in the transactions table: %s", channelName, err.Error()) - } - return rpcSendResp, nil -} diff --git a/internal/tss/channels/webhook_handler_service_channel.go b/internal/tss/channels/webhook_channel.go similarity index 91% rename from internal/tss/channels/webhook_handler_service_channel.go rename to internal/tss/channels/webhook_channel.go index 49ddde5..c005a51 100644 --- a/internal/tss/channels/webhook_handler_service_channel.go +++ b/internal/tss/channels/webhook_channel.go @@ -9,7 +9,8 @@ import ( "github.com/alitto/pond" "github.com/stellar/go/support/log" "github.com/stellar/wallet-backend/internal/tss" - "github.com/stellar/wallet-backend/internal/tss/utils" + tssutils "github.com/stellar/wallet-backend/internal/tss/utils" + "github.com/stellar/wallet-backend/internal/utils" ) type WebhookHandlerServiceChannelConfigs struct { @@ -47,7 +48,7 @@ func (p *webhookHandlerServicePool) Send(payload tss.Payload) { } func (p *webhookHandlerServicePool) Receive(payload tss.Payload) { - resp := utils.PayloadTOTSSResponse(payload) + resp := tssutils.PayloadTOTSSResponse(payload) jsonData, err := json.Marshal(resp) if err != nil { log.Errorf("WebhookHandlerServiceChannel: error marshaling payload: %s", err.Error()) diff --git a/internal/tss/channels/webhook_handler_service_channel_test.go b/internal/tss/channels/webhook_channel_test.go similarity index 86% rename from internal/tss/channels/webhook_handler_service_channel_test.go rename to internal/tss/channels/webhook_channel_test.go index df391e4..56d3235 100644 --- a/internal/tss/channels/webhook_handler_service_channel_test.go +++ b/internal/tss/channels/webhook_channel_test.go @@ -9,7 +9,8 @@ import ( "testing" "github.com/stellar/wallet-backend/internal/tss" - "github.com/stellar/wallet-backend/internal/tss/utils" + tssutils "github.com/stellar/wallet-backend/internal/tss/utils" + "github.com/stellar/wallet-backend/internal/utils" ) func TestWebhookHandlerServiceChannel(t *testing.T) { @@ -25,7 +26,7 @@ func TestWebhookHandlerServiceChannel(t *testing.T) { payload := tss.Payload{} payload.WebhookURL = "www.stellar.org" - jsonData, _ := json.Marshal(utils.PayloadTOTSSResponse(payload)) + jsonData, _ := json.Marshal(tssutils.PayloadTOTSSResponse(payload)) httpResponse1 := &http.Response{ StatusCode: http.StatusBadGateway, diff --git a/internal/tss/router/mocks.go b/internal/tss/router/mocks.go index 3f4406c..2f269b7 100644 --- a/internal/tss/router/mocks.go +++ b/internal/tss/router/mocks.go @@ -11,6 +11,7 @@ type MockRouter struct { var _ Router = (*MockRouter)(nil) -func (r *MockRouter) Route(payload tss.Payload) { - r.Called(payload) +func (r *MockRouter) Route(payload tss.Payload) error { + args := r.Called(payload) + return args.Error(0) } diff --git a/internal/tss/router/router.go b/internal/tss/router/router.go index dcbd2db..8c6801a 100644 --- a/internal/tss/router/router.go +++ b/internal/tss/router/router.go @@ -1,67 +1,68 @@ package router import ( + "fmt" "slices" - "github.com/stellar/go/xdr" + "github.com/stellar/wallet-backend/internal/entities" "github.com/stellar/wallet-backend/internal/tss" - "github.com/stellar/wallet-backend/internal/tss/services" ) type Router interface { - Route(payload tss.Payload) + Route(payload tss.Payload) error } type RouterConfigs struct { - ErrorHandlerService services.Service - WebhookHandlerService services.Service + RPCCallerChannel tss.Channel + ErrorJitterChannel tss.Channel + ErrorNonJitterChannel tss.Channel + WebhookChannel tss.Channel } type router struct { - ErrorHandlerService services.Service - WebhookHandlerService services.Service + RPCCallerChannel tss.Channel + ErrorJitterChannel tss.Channel + ErrorNonJitterChannel tss.Channel + WebhookChannel tss.Channel } var _ Router = (*router)(nil) -var FinalErrorCodes = []xdr.TransactionResultCode{ - xdr.TransactionResultCodeTxSuccess, - xdr.TransactionResultCodeTxFailed, - xdr.TransactionResultCodeTxMissingOperation, - xdr.TransactionResultCodeTxInsufficientBalance, - xdr.TransactionResultCodeTxBadAuthExtra, - xdr.TransactionResultCodeTxMalformed, -} - -var RetryErrorCodes = []xdr.TransactionResultCode{ - xdr.TransactionResultCodeTxTooLate, - xdr.TransactionResultCodeTxInsufficientFee, - xdr.TransactionResultCodeTxInternalError, - xdr.TransactionResultCodeTxBadSeq, -} - func NewRouter(cfg RouterConfigs) Router { return &router{ - ErrorHandlerService: cfg.ErrorHandlerService, - WebhookHandlerService: cfg.WebhookHandlerService, + RPCCallerChannel: cfg.RPCCallerChannel, + ErrorJitterChannel: cfg.ErrorJitterChannel, + ErrorNonJitterChannel: cfg.ErrorNonJitterChannel, + WebhookChannel: cfg.WebhookChannel, } } -func (r *router) Route(payload tss.Payload) { - switch payload.RpcSubmitTxResponse.Status { - case tss.TryAgainLaterStatus: - r.ErrorHandlerService.ProcessPayload(payload) - case tss.ErrorStatus: - if payload.RpcSubmitTxResponse.Code.OtherCodes == tss.NoCode { - if slices.Contains(RetryErrorCodes, payload.RpcSubmitTxResponse.Code.TxResultCode) { - r.ErrorHandlerService.ProcessPayload(payload) - } else if slices.Contains(FinalErrorCodes, payload.RpcSubmitTxResponse.Code.TxResultCode) { - r.WebhookHandlerService.ProcessPayload(payload) +func (r *router) Route(payload tss.Payload) error { + var channel tss.Channel + if payload.RpcSubmitTxResponse.Status.Status() != "" { + switch payload.RpcSubmitTxResponse.Status.Status() { + case string(tss.NewStatus): + channel = r.RPCCallerChannel + case string(entities.TryAgainLaterStatus): + channel = r.ErrorJitterChannel + case string(entities.ErrorStatus): + if payload.RpcSubmitTxResponse.Code.OtherCodes == tss.NoCode { + if slices.Contains(tss.JitterErrorCodes, payload.RpcSubmitTxResponse.Code.TxResultCode) { + channel = r.ErrorJitterChannel + } else if slices.Contains(tss.NonJitterErrorCodes, payload.RpcSubmitTxResponse.Code.TxResultCode) { + channel = r.ErrorNonJitterChannel + } else if slices.Contains(tss.FinalErrorCodes, payload.RpcSubmitTxResponse.Code.TxResultCode) { + channel = r.WebhookChannel + } } + default: + // Do nothing for PENDING / DUPLICATE statuses + return nil } - // if Code.OtherCodes = {RPCFailCode, UnMarshall, do nothing, as this should be rare. Let the ticker task take care of this} - default: - // PENDING = wait to ingest this transaction via getTransactions() - return } + if channel == nil { + return fmt.Errorf("payload could not be routed - channel is nil") + } + channel.Send(payload) + return nil } diff --git a/internal/tss/router/router_test.go b/internal/tss/router/router_test.go index 8a5907a..58eea6f 100644 --- a/internal/tss/router/router_test.go +++ b/internal/tss/router/router_test.go @@ -3,49 +3,129 @@ package router import ( "testing" - "github.com/stellar/go/xdr" + "github.com/stellar/wallet-backend/internal/entities" "github.com/stellar/wallet-backend/internal/tss" - "github.com/stellar/wallet-backend/internal/tss/services" + "github.com/stretchr/testify/assert" ) func TestRouter(t *testing.T) { - errorHandlerService := services.MockService{} - defer errorHandlerService.AssertExpectations(t) - webhookHandlerService := services.MockService{} - router := NewRouter(RouterConfigs{ErrorHandlerService: &errorHandlerService, WebhookHandlerService: &webhookHandlerService}) - t.Run("status_try_again_later", func(t *testing.T) { + rpcCallerChannel := tss.MockChannel{} + defer rpcCallerChannel.AssertExpectations(t) + errorJitterChannel := tss.MockChannel{} + defer errorJitterChannel.AssertExpectations(t) + errorNonJitterChannel := tss.MockChannel{} + defer errorNonJitterChannel.AssertExpectations(t) + webhookChannel := tss.MockChannel{} + defer webhookChannel.AssertExpectations(t) + + router := NewRouter(RouterConfigs{ + RPCCallerChannel: &rpcCallerChannel, + ErrorJitterChannel: &errorJitterChannel, + ErrorNonJitterChannel: &errorNonJitterChannel, + WebhookChannel: &webhookChannel, + }) + t.Run("status_new_routes_to_rpc_caller_channel", func(t *testing.T) { payload := tss.Payload{} - payload.RpcSubmitTxResponse.Status = tss.TryAgainLaterStatus + payload.RpcSubmitTxResponse.Status = tss.RPCTXStatus{OtherStatus: tss.NewStatus} - errorHandlerService. - On("ProcessPayload", payload). + rpcCallerChannel. + On("Send", payload). Return(). Once() - router.Route(payload) + _ = router.Route(payload) + + rpcCallerChannel.AssertCalled(t, "Send", payload) }) - t.Run("error_status_route_to_error_handler_service", func(t *testing.T) { + t.Run("status_try_again_later_routes_to_error_jitter_channel", func(t *testing.T) { payload := tss.Payload{} - payload.RpcSubmitTxResponse.Status = tss.ErrorStatus - payload.RpcSubmitTxResponse.Code.TxResultCode = xdr.TransactionResultCodeTxInsufficientFee + payload.RpcSubmitTxResponse.Status = tss.RPCTXStatus{RPCStatus: entities.TryAgainLaterStatus} - errorHandlerService. - On("ProcessPayload", payload). + errorJitterChannel. + On("Send", payload). Return(). Once() - router.Route(payload) + _ = router.Route(payload) + + errorJitterChannel.AssertCalled(t, "Send", payload) + }) + t.Run("status_error_routes_to_error_jitter_channel", func(t *testing.T) { + for _, code := range tss.JitterErrorCodes { + payload := tss.Payload{ + RpcSubmitTxResponse: tss.RPCSendTxResponse{ + Status: tss.RPCTXStatus{ + RPCStatus: entities.ErrorStatus, + }, + Code: tss.RPCTXCode{ + TxResultCode: code, + }, + }, + } + payload.RpcSubmitTxResponse.Code.TxResultCode = code + errorJitterChannel. + On("Send", payload). + Return(). + Once() + + _ = router.Route(payload) + + errorJitterChannel.AssertCalled(t, "Send", payload) + } + }) + t.Run("status_error_routes_to_error_non_jitter_channel", func(t *testing.T) { + + for _, code := range tss.NonJitterErrorCodes { + payload := tss.Payload{ + RpcSubmitTxResponse: tss.RPCSendTxResponse{ + Status: tss.RPCTXStatus{ + RPCStatus: entities.ErrorStatus, + }, + Code: tss.RPCTXCode{ + TxResultCode: code, + }, + }, + } + payload.RpcSubmitTxResponse.Code.TxResultCode = code + errorNonJitterChannel. + On("Send", payload). + Return(). + Once() + + _ = router.Route(payload) + + errorNonJitterChannel.AssertCalled(t, "Send", payload) + } + }) + t.Run("status_error_routes_to_webhook_channel", func(t *testing.T) { + for _, code := range tss.FinalErrorCodes { + payload := tss.Payload{ + RpcSubmitTxResponse: tss.RPCSendTxResponse{ + Status: tss.RPCTXStatus{ + RPCStatus: entities.ErrorStatus, + }, + Code: tss.RPCTXCode{ + TxResultCode: code, + }, + }, + } + payload.RpcSubmitTxResponse.Code.TxResultCode = code + webhookChannel. + On("Send", payload). + Return(). + Once() + + _ = router.Route(payload) + + webhookChannel.AssertCalled(t, "Send", payload) + } }) - t.Run("error_status_route_to_webhook_handler_service", func(t *testing.T) { + t.Run("nil_channel_does_not_route", func(t *testing.T) { payload := tss.Payload{} - payload.RpcSubmitTxResponse.Status = tss.ErrorStatus - payload.RpcSubmitTxResponse.Code.TxResultCode = xdr.TransactionResultCodeTxInsufficientBalance - webhookHandlerService. - On("ProcessPayload", payload). - Return(). - Once() + err := router.Route(payload) - router.Route(payload) + errorJitterChannel.AssertNotCalled(t, "Send", payload) + assert.Equal(t, "payload could not be routed - channel is nil", err.Error()) }) } diff --git a/internal/tss/services/error_handler_service.go b/internal/tss/services/error_handler_service.go deleted file mode 100644 index e24c310..0000000 --- a/internal/tss/services/error_handler_service.go +++ /dev/null @@ -1,36 +0,0 @@ -package services - -import ( - "slices" - - "github.com/stellar/wallet-backend/internal/tss" -) - -type errorHandlerService struct { - JitterChannel tss.Channel - NonJitterChannel tss.Channel -} - -type ErrorHandlerServiceConfigs struct { - JitterChannel tss.Channel - NonJitterChannel tss.Channel -} - -func NewErrorHandlerService(cfg ErrorHandlerServiceConfigs) *errorHandlerService { - return &errorHandlerService{ - JitterChannel: cfg.JitterChannel, - NonJitterChannel: cfg.NonJitterChannel, - } -} - -func (p *errorHandlerService) ProcessPayload(payload tss.Payload) { - if payload.RpcSubmitTxResponse.Status == tss.TryAgainLaterStatus { - p.JitterChannel.Send(payload) - } else { - if slices.Contains(tss.NonJitterErrorCodes, payload.RpcSubmitTxResponse.Code.TxResultCode) { - p.NonJitterChannel.Send(payload) - } else if slices.Contains(tss.JitterErrorCodes, payload.RpcSubmitTxResponse.Code.TxResultCode) { - p.JitterChannel.Send(payload) - } - } -} diff --git a/internal/tss/services/error_handler_service_test.go b/internal/tss/services/error_handler_service_test.go deleted file mode 100644 index 802cc8c..0000000 --- a/internal/tss/services/error_handler_service_test.go +++ /dev/null @@ -1,54 +0,0 @@ -package services - -import ( - "testing" - - "github.com/stellar/go/xdr" - "github.com/stellar/wallet-backend/internal/tss" -) - -func TestProcessPayload(t *testing.T) { - jitterChannel := tss.MockChannel{} - defer jitterChannel.AssertExpectations(t) - nonJitterChannel := tss.MockChannel{} - defer nonJitterChannel.AssertExpectations(t) - - service := NewErrorHandlerService(ErrorHandlerServiceConfigs{JitterChannel: &jitterChannel, NonJitterChannel: &nonJitterChannel}) - - t.Run("status_try_again_later", func(t *testing.T) { - payload := tss.Payload{} - payload.RpcSubmitTxResponse.Status = tss.TryAgainLaterStatus - - jitterChannel. - On("Send", payload). - Return(). - Once() - - service.ProcessPayload(payload) - }) - t.Run("code_tx_too_early", func(t *testing.T) { - payload := tss.Payload{} - payload.RpcSubmitTxResponse.Status = tss.ErrorStatus - payload.RpcSubmitTxResponse.Code.TxResultCode = xdr.TransactionResultCodeTxTooEarly - - nonJitterChannel. - On("Send", payload). - Return(). - Once() - - service.ProcessPayload(payload) - }) - - t.Run("code_tx_insufficient_fee", func(t *testing.T) { - payload := tss.Payload{} - payload.RpcSubmitTxResponse.Status = tss.ErrorStatus - payload.RpcSubmitTxResponse.Code.TxResultCode = xdr.TransactionResultCodeTxInsufficientFee - - jitterChannel. - On("Send", payload). - Return(). - Once() - - service.ProcessPayload(payload) - }) -} diff --git a/internal/tss/services/mocks.go b/internal/tss/services/mocks.go index fff8db8..3edcb26 100644 --- a/internal/tss/services/mocks.go +++ b/internal/tss/services/mocks.go @@ -1,16 +1,51 @@ package services import ( + "context" + + "github.com/stellar/go/txnbuild" "github.com/stellar/wallet-backend/internal/tss" + "github.com/stretchr/testify/mock" ) -type MockService struct { +type TransactionServiceMock struct { + mock.Mock +} + +var _ TransactionService = (*TransactionServiceMock)(nil) + +func (t *TransactionServiceMock) NetworkPassphrase() string { + args := t.Called() + return args.String(0) +} + +func (t *TransactionServiceMock) SignAndBuildNewFeeBumpTransaction(ctx context.Context, origTxXdr string) (*txnbuild.FeeBumpTransaction, error) { + args := t.Called(ctx, origTxXdr) + if result := args.Get(0); result != nil { + return result.(*txnbuild.FeeBumpTransaction), args.Error(1) + } + return nil, args.Error(1) + +} + +func (t *TransactionServiceMock) SendTransaction(transactionXdr string) (tss.RPCSendTxResponse, error) { + args := t.Called(transactionXdr) + return args.Get(0).(tss.RPCSendTxResponse), args.Error(1) +} + +func (t *TransactionServiceMock) GetTransaction(transactionHash string) (tss.RPCGetIngestTxResponse, error) { + args := t.Called(transactionHash) + return args.Get(0).(tss.RPCGetIngestTxResponse), args.Error(1) +} + +type TransactionManagerMock struct { mock.Mock } -var _ Service = (*MockService)(nil) +var _ TransactionManager = (*TransactionManagerMock)(nil) -func (s *MockService) ProcessPayload(payload tss.Payload) { - s.Called(payload) +func (t *TransactionManagerMock) BuildAndSubmitTransaction(ctx context.Context, channelName string, payload tss.Payload) (tss.RPCSendTxResponse, error) { + args := t.Called(ctx, channelName, payload) + return args.Get(0).(tss.RPCSendTxResponse), args.Error(1) } diff --git a/internal/tss/services/rpc_caller_service.go b/internal/tss/services/rpc_caller_service.go deleted file mode 100644 index 7f539eb..0000000 --- a/internal/tss/services/rpc_caller_service.go +++ /dev/null @@ -1,21 +0,0 @@ -package services - -import ( - "github.com/stellar/wallet-backend/internal/tss" -) - -type rpcCallerService struct { - channel tss.Channel -} - -var _ Service = (*rpcCallerService)(nil) - -func NewRPCCallerService(channel tss.Channel) Service { - return &rpcCallerService{ - channel: channel, - } -} - -func (p *rpcCallerService) ProcessPayload(payload tss.Payload) { - p.channel.Send(payload) -} diff --git a/internal/tss/services/transaction_manager.go b/internal/tss/services/transaction_manager.go new file mode 100644 index 0000000..7825582 --- /dev/null +++ b/internal/tss/services/transaction_manager.go @@ -0,0 +1,76 @@ +package services + +import ( + "context" + "fmt" + + "github.com/stellar/wallet-backend/internal/services" + "github.com/stellar/wallet-backend/internal/tss" + "github.com/stellar/wallet-backend/internal/tss/store" +) + +type TransactionManager interface { + BuildAndSubmitTransaction(ctx context.Context, channelName string, payload tss.Payload) (tss.RPCSendTxResponse, error) +} + +type TransactionManagerConfigs struct { + TxService TransactionService + RPCService services.RPCService + Store store.Store +} + +type transactionManager struct { + TxService TransactionService + RPCService services.RPCService + Store store.Store +} + +func NewTransactionManager(cfg TransactionManagerConfigs) *transactionManager { + return &transactionManager{ + TxService: cfg.TxService, + RPCService: cfg.RPCService, + Store: cfg.Store, + } +} + +func (t *transactionManager) BuildAndSubmitTransaction(ctx context.Context, channelName string, payload tss.Payload) (tss.RPCSendTxResponse, error) { + feeBumpTx, err := t.TxService.SignAndBuildNewFeeBumpTransaction(ctx, payload.TransactionXDR) + if err != nil { + return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to sign/build transaction: %w", channelName, err) + } + feeBumpTxHash, err := feeBumpTx.HashHex(t.TxService.NetworkPassphrase()) + if err != nil { + return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to hashhex fee bump transaction: %w", channelName, err) + } + + feeBumpTxXDR, err := feeBumpTx.Base64() + if err != nil { + return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to base64 fee bump transaction: %w", channelName, err) + } + + err = t.Store.UpsertTry(ctx, payload.TransactionHash, feeBumpTxHash, feeBumpTxXDR, tss.RPCTXCode{OtherCodes: tss.NewCode}) + if err != nil { + return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to upsert try in tries table: %w", channelName, err) + } + rpcResp, rpcErr := t.RPCService.SendTransaction(feeBumpTxXDR) + rpcSendResp, parseErr := tss.ParseToRPCSendTxResponse(feeBumpTxHash, rpcResp, rpcErr) + + err = t.Store.UpsertTry(ctx, payload.TransactionHash, feeBumpTxHash, feeBumpTxXDR, rpcSendResp.Code) + if err != nil { + return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to upsert try in tries table: %s", channelName, err.Error()) + } + + if parseErr != nil { + return rpcSendResp, fmt.Errorf("%s: RPC fail: %w", channelName, parseErr) + } + + if rpcErr != nil && rpcSendResp.Code.OtherCodes == tss.RPCFailCode || rpcSendResp.Code.OtherCodes == tss.UnmarshalBinaryCode { + return tss.RPCSendTxResponse{}, fmt.Errorf("%s: RPC fail: %w", channelName, rpcErr) + } + + err = t.Store.UpsertTransaction(ctx, payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, rpcSendResp.Status) + if err != nil { + return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to do the final update of tx in the transactions table: %s", channelName, err.Error()) + } + return rpcSendResp, nil +} diff --git a/internal/tss/channels/utils_test.go b/internal/tss/services/transaction_manager_test.go similarity index 50% rename from internal/tss/channels/utils_test.go rename to internal/tss/services/transaction_manager_test.go index c3dcb97..80731d4 100644 --- a/internal/tss/channels/utils_test.go +++ b/internal/tss/services/transaction_manager_test.go @@ -1,4 +1,4 @@ -package channels +package services import ( "context" @@ -8,6 +8,8 @@ import ( "github.com/stellar/go/xdr" "github.com/stellar/wallet-backend/internal/db" "github.com/stellar/wallet-backend/internal/db/dbtest" + "github.com/stellar/wallet-backend/internal/entities" + "github.com/stellar/wallet-backend/internal/services/servicesmocks" "github.com/stellar/wallet-backend/internal/tss" "github.com/stellar/wallet-backend/internal/tss/store" "github.com/stellar/wallet-backend/internal/tss/utils" @@ -23,7 +25,13 @@ func TestBuildAndSubmitTransaction(t *testing.T) { require.NoError(t, err) defer dbConnectionPool.Close() store := store.NewStore(dbConnectionPool) - txServiceMock := utils.TransactionServiceMock{} + txServiceMock := TransactionServiceMock{} + rpcServiceMock := servicesmocks.RPCServiceMock{} + txManager := NewTransactionManager(TransactionManagerConfigs{ + TxService: &txServiceMock, + RPCService: &rpcServiceMock, + Store: store, + }) networkPass := "passphrase" feeBumpTx := utils.BuildTestFeeBumpTransaction() feeBumpTxXDR, _ := feeBumpTx.Base64() @@ -33,14 +41,14 @@ func TestBuildAndSubmitTransaction(t *testing.T) { payload.TransactionHash = "hash" payload.TransactionXDR = "xdr" - _ = store.UpsertTransaction(context.Background(), payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.NewStatus) t.Run("fail_on_tx_build_and_sign", func(t *testing.T) { + _ = store.UpsertTransaction(context.Background(), payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.RPCTXStatus{OtherStatus: tss.NewStatus}) txServiceMock. On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). Return(nil, errors.New("signing failed")). Once() - _, err := BuildAndSubmitTransaction(context.Background(), "channel", payload, store, &txServiceMock) + _, err := txManager.BuildAndSubmitTransaction(context.Background(), "channel", payload) assert.Equal(t, "channel: Unable to sign/build transaction: signing failed", err.Error()) @@ -51,22 +59,24 @@ func TestBuildAndSubmitTransaction(t *testing.T) { }) t.Run("rpc_call_fail", func(t *testing.T) { - sendResp := tss.RPCSendTxResponse{} - sendResp.Code.OtherCodes = tss.RPCFailCode + _ = store.UpsertTransaction(context.Background(), payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.RPCTXStatus{OtherStatus: tss.NewStatus}) + sendResp := entities.RPCSendTransactionResult{Status: entities.ErrorStatus} + txServiceMock. On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). Return(feeBumpTx, nil). Once(). On("NetworkPassphrase"). Return(networkPass). - Once(). + Once() + rpcServiceMock. On("SendTransaction", feeBumpTxXDR). - Return(sendResp, errors.New("RPC Fail")). + Return(sendResp, errors.New("RPC down")). Once() - _, err := BuildAndSubmitTransaction(context.Background(), "channel", payload, store, &txServiceMock) + _, err := txManager.BuildAndSubmitTransaction(context.Background(), "channel", payload) - assert.Equal(t, "channel: RPC fail: RPC Fail", err.Error()) + assert.Equal(t, "channel: RPC fail: RPC fail: RPC down", err.Error()) var txStatus string err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) @@ -79,23 +89,63 @@ func TestBuildAndSubmitTransaction(t *testing.T) { assert.Equal(t, int(tss.RPCFailCode), tryStatus) }) - t.Run("rpc_resp_unmarshaling_error", func(t *testing.T) { - sendResp := tss.RPCSendTxResponse{} - sendResp.Code.OtherCodes = tss.UnMarshalBinaryCode + t.Run("rpc_resp_empty_errorresult_xdr", func(t *testing.T) { + _ = store.UpsertTransaction(context.Background(), payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.RPCTXStatus{OtherStatus: tss.NewStatus}) + sendResp := entities.RPCSendTransactionResult{ + Status: entities.PendingStatus, + ErrorResultXDR: "", + } + txServiceMock. On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). Return(feeBumpTx, nil). Once(). On("NetworkPassphrase"). Return(networkPass). + Once() + rpcServiceMock. + On("SendTransaction", feeBumpTxXDR). + Return(sendResp, nil). + Once() + + resp, err := txManager.BuildAndSubmitTransaction(context.Background(), "channel", payload) + + assert.Equal(t, entities.PendingStatus, resp.Status.RPCStatus) + assert.Equal(t, tss.EmptyCode, resp.Code.OtherCodes) + assert.Empty(t, err) + + var txStatus string + err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) + require.NoError(t, err) + assert.Equal(t, txStatus, string(entities.PendingStatus)) + + var tryStatus int + err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) + require.NoError(t, err) + assert.Equal(t, int(tss.EmptyCode), tryStatus) + }) + t.Run("rpc_resp_has_unparsable_errorresult_xdr", func(t *testing.T) { + _ = store.UpsertTransaction(context.Background(), payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.RPCTXStatus{OtherStatus: tss.NewStatus}) + sendResp := entities.RPCSendTransactionResult{ + Status: entities.ErrorStatus, + ErrorResultXDR: "ABCD", + } + + txServiceMock. + On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). + Return(feeBumpTx, nil). Once(). + On("NetworkPassphrase"). + Return(networkPass). + Once() + rpcServiceMock. On("SendTransaction", feeBumpTxXDR). - Return(sendResp, errors.New("unable to unmarshal")). + Return(sendResp, nil). Once() - _, err := BuildAndSubmitTransaction(context.Background(), "channel", payload, store, &txServiceMock) + _, err := txManager.BuildAndSubmitTransaction(context.Background(), "channel", payload) - assert.Equal(t, "channel: RPC fail: unable to unmarshal", err.Error()) + assert.Equal(t, "channel: RPC fail: parse error result xdr string: unable to unmarshal errorResultXDR: ABCD", err.Error()) var txStatus string err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) @@ -105,39 +155,41 @@ func TestBuildAndSubmitTransaction(t *testing.T) { var tryStatus int err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) require.NoError(t, err) - assert.Equal(t, int(tss.UnMarshalBinaryCode), tryStatus) + assert.Equal(t, int(tss.UnmarshalBinaryCode), tryStatus) }) t.Run("rpc_returns_response", func(t *testing.T) { - sendResp := tss.RPCSendTxResponse{} - sendResp.Status = tss.TryAgainLaterStatus - sendResp.TransactionHash = feeBumpTxHash - sendResp.TransactionXDR = feeBumpTxXDR - sendResp.Code.TxResultCode = xdr.TransactionResultCodeTxInsufficientFee + _ = store.UpsertTransaction(context.Background(), payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.RPCTXStatus{OtherStatus: tss.NewStatus}) + sendResp := entities.RPCSendTransactionResult{ + Status: entities.ErrorStatus, + ErrorResultXDR: "AAAAAAAAAMj////9AAAAAA==", + } + txServiceMock. On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). Return(feeBumpTx, nil). Once(). On("NetworkPassphrase"). Return(networkPass). - Once(). + Once() + rpcServiceMock. On("SendTransaction", feeBumpTxXDR). Return(sendResp, nil). Once() - resp, err := BuildAndSubmitTransaction(context.Background(), "channel", payload, store, &txServiceMock) + resp, err := txManager.BuildAndSubmitTransaction(context.Background(), "channel", payload) - assert.Equal(t, tss.TryAgainLaterStatus, resp.Status) - assert.Equal(t, xdr.TransactionResultCodeTxInsufficientFee, resp.Code.TxResultCode) + assert.Equal(t, entities.ErrorStatus, resp.Status.RPCStatus) + assert.Equal(t, xdr.TransactionResultCodeTxTooLate, resp.Code.TxResultCode) assert.Empty(t, err) var txStatus string err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) require.NoError(t, err) - assert.Equal(t, string(tss.TryAgainLaterStatus), txStatus) + assert.Equal(t, string(entities.ErrorStatus), txStatus) var tryStatus int err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) require.NoError(t, err) - assert.Equal(t, int(xdr.TransactionResultCodeTxInsufficientFee), tryStatus) + assert.Equal(t, int(xdr.TransactionResultCodeTxTooLate), tryStatus) }) } diff --git a/internal/tss/utils/transaction_service.go b/internal/tss/services/transaction_service.go similarity index 50% rename from internal/tss/utils/transaction_service.go rename to internal/tss/services/transaction_service.go index 129c1f0..b65bae1 100644 --- a/internal/tss/utils/transaction_service.go +++ b/internal/tss/services/transaction_service.go @@ -1,42 +1,25 @@ -package utils +package services import ( - "bytes" "context" - "encoding/base64" - "encoding/json" "fmt" - "io" - "net/http" - "strconv" - xdr3 "github.com/stellar/go-xdr/xdr3" "github.com/stellar/go/clients/horizonclient" "github.com/stellar/go/txnbuild" - "github.com/stellar/go/xdr" "github.com/stellar/wallet-backend/internal/signing" - "github.com/stellar/wallet-backend/internal/tss" tsserror "github.com/stellar/wallet-backend/internal/tss/errors" ) -type HTTPClient interface { - Post(url string, t string, body io.Reader) (resp *http.Response, err error) -} - type TransactionService interface { NetworkPassphrase() string SignAndBuildNewFeeBumpTransaction(ctx context.Context, origTxXdr string) (*txnbuild.FeeBumpTransaction, error) - SendTransaction(transactionXdr string) (tss.RPCSendTxResponse, error) - GetTransaction(transactionHash string) (tss.RPCGetIngestTxResponse, error) } type transactionService struct { DistributionAccountSignatureClient signing.SignatureClient ChannelAccountSignatureClient signing.SignatureClient HorizonClient horizonclient.ClientInterface - RPCURL string BaseFee int64 - HTTPClient HTTPClient } var _ TransactionService = (*transactionService)(nil) @@ -45,9 +28,7 @@ type TransactionServiceOptions struct { DistributionAccountSignatureClient signing.SignatureClient ChannelAccountSignatureClient signing.SignatureClient HorizonClient horizonclient.ClientInterface - RPCURL string BaseFee int64 - HTTPClient HTTPClient } func (o *TransactionServiceOptions) ValidateOptions() error { @@ -63,18 +44,10 @@ func (o *TransactionServiceOptions) ValidateOptions() error { return fmt.Errorf("horizon client cannot be nil") } - if o.RPCURL == "" { - return fmt.Errorf("rpc url cannot be empty") - } - if o.BaseFee < int64(txnbuild.MinBaseFee) { return fmt.Errorf("base fee is lower than the minimum network fee") } - if o.HTTPClient == nil { - return fmt.Errorf("http client cannot be nil") - } - return nil } @@ -86,9 +59,7 @@ func NewTransactionService(opts TransactionServiceOptions) (*transactionService, DistributionAccountSignatureClient: opts.DistributionAccountSignatureClient, ChannelAccountSignatureClient: opts.ChannelAccountSignatureClient, HorizonClient: opts.HorizonClient, - RPCURL: opts.RPCURL, BaseFee: opts.BaseFee, - HTTPClient: opts.HTTPClient, }, nil } @@ -154,84 +125,3 @@ func (t *transactionService) SignAndBuildNewFeeBumpTransaction(ctx context.Conte } return feeBumpTx, nil } - -func (t *transactionService) parseErrorResultXDR(errorResultXdr string) (tss.RPCTXCode, error) { - unMarshalErr := "unable to unmarshal errorResultXdr: %s" - decodedBytes, err := base64.StdEncoding.DecodeString(errorResultXdr) - if err != nil { - return tss.RPCTXCode{OtherCodes: tss.UnMarshalBinaryCode}, fmt.Errorf(unMarshalErr, errorResultXdr) - } - var errorResult xdr.TransactionResult - _, err = xdr3.Unmarshal(bytes.NewReader(decodedBytes), &errorResult) - if err != nil { - return tss.RPCTXCode{OtherCodes: tss.UnMarshalBinaryCode}, fmt.Errorf(unMarshalErr, errorResultXdr) - } - return tss.RPCTXCode{ - TxResultCode: errorResult.Result.Code, - }, nil -} - -func (t *transactionService) sendRPCRequest(method string, params map[string]string) (tss.RPCResponse, error) { - payload := map[string]interface{}{ - "jsonrpc": "2.0", - "id": 1, - "method": method, - "params": params, - } - jsonData, err := json.Marshal(payload) - - if err != nil { - return tss.RPCResponse{}, fmt.Errorf("marshaling payload") - } - - resp, err := t.HTTPClient.Post(t.RPCURL, "application/json", bytes.NewBuffer(jsonData)) - if err != nil { - return tss.RPCResponse{}, fmt.Errorf("%s: sending POST request to rpc: %v", method, err) - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return tss.RPCResponse{}, fmt.Errorf("%s: unmarshaling RPC response", method) - } - var res tss.RPCResponse - err = json.Unmarshal(body, &res) - if err != nil { - return tss.RPCResponse{}, fmt.Errorf("%s: parsing RPC response JSON", method) - } - return res, nil -} - -func (t *transactionService) SendTransaction(transactionXdr string) (tss.RPCSendTxResponse, error) { - rpcResponse, err := t.sendRPCRequest("sendTransaction", map[string]string{"transaction": transactionXdr}) - sendTxResponse := tss.RPCSendTxResponse{} - sendTxResponse.TransactionXDR = transactionXdr - if err != nil { - sendTxResponse.Status = tss.ErrorStatus - sendTxResponse.Code.OtherCodes = tss.RPCFailCode - return sendTxResponse, fmt.Errorf("RPC fail: %s", err.Error()) - } - sendTxResponse.Status = tss.RPCTXStatus(rpcResponse.RPCResult.Status) - sendTxResponse.Code, err = t.parseErrorResultXDR(rpcResponse.RPCResult.ErrorResultXDR) - sendTxResponse.TransactionHash = rpcResponse.RPCResult.Hash - return sendTxResponse, err -} - -func (t *transactionService) GetTransaction(transactionHash string) (tss.RPCGetIngestTxResponse, error) { - rpcResponse, err := t.sendRPCRequest("getTransaction", map[string]string{"hash": transactionHash}) - if err != nil { - return tss.RPCGetIngestTxResponse{Status: tss.ErrorStatus}, fmt.Errorf("RPC Fail: %s", err.Error()) - } - getIngestTxResponse := tss.RPCGetIngestTxResponse{} - getIngestTxResponse.Status = tss.RPCTXStatus(rpcResponse.RPCResult.Status) - getIngestTxResponse.EnvelopeXDR = rpcResponse.RPCResult.EnvelopeXDR - getIngestTxResponse.ResultXDR = rpcResponse.RPCResult.ResultXDR - if getIngestTxResponse.Status != tss.NotFoundStatus { - getIngestTxResponse.CreatedAt, err = strconv.ParseInt(rpcResponse.RPCResult.CreatedAt, 10, 64) - if err != nil { - return tss.RPCGetIngestTxResponse{Status: tss.ErrorStatus}, fmt.Errorf("unable to parse createAt: %s", err.Error()) - } - } - getIngestTxResponse.Code, err = t.parseErrorResultXDR(rpcResponse.RPCResult.ResultXDR) - return getIngestTxResponse, err -} diff --git a/internal/tss/services/transaction_service_test.go b/internal/tss/services/transaction_service_test.go new file mode 100644 index 0000000..4dd137c --- /dev/null +++ b/internal/tss/services/transaction_service_test.go @@ -0,0 +1,236 @@ +package services + +import ( + "context" + "errors" + "testing" + + "github.com/stellar/go/clients/horizonclient" + "github.com/stellar/go/keypair" + "github.com/stellar/go/protocols/horizon" + "github.com/stellar/go/txnbuild" + "github.com/stellar/wallet-backend/internal/signing" + tsserror "github.com/stellar/wallet-backend/internal/tss/errors" + "github.com/stellar/wallet-backend/internal/tss/utils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestValidateOptions(t *testing.T) { + t.Run("return_error_when_distribution_signature_client_nil", func(t *testing.T) { + opts := TransactionServiceOptions{ + DistributionAccountSignatureClient: nil, + ChannelAccountSignatureClient: &signing.SignatureClientMock{}, + HorizonClient: &horizonclient.MockClient{}, + BaseFee: 114, + } + err := opts.ValidateOptions() + assert.Equal(t, "distribution account signature client cannot be nil", err.Error()) + + }) + + t.Run("return_error_when_channel_signature_client_nil", func(t *testing.T) { + opts := TransactionServiceOptions{ + DistributionAccountSignatureClient: &signing.SignatureClientMock{}, + ChannelAccountSignatureClient: nil, + HorizonClient: &horizonclient.MockClient{}, + BaseFee: 114, + } + err := opts.ValidateOptions() + assert.Equal(t, "channel account signature client cannot be nil", err.Error()) + }) + + t.Run("return_error_when_horizon_client_nil", func(t *testing.T) { + opts := TransactionServiceOptions{ + DistributionAccountSignatureClient: &signing.SignatureClientMock{}, + ChannelAccountSignatureClient: &signing.SignatureClientMock{}, + HorizonClient: nil, + BaseFee: 114, + } + err := opts.ValidateOptions() + assert.Equal(t, "horizon client cannot be nil", err.Error()) + }) + + t.Run("return_error_when_base_fee_too_low", func(t *testing.T) { + opts := TransactionServiceOptions{ + DistributionAccountSignatureClient: &signing.SignatureClientMock{}, + ChannelAccountSignatureClient: &signing.SignatureClientMock{}, + HorizonClient: &horizonclient.MockClient{}, + BaseFee: txnbuild.MinBaseFee - 10, + } + err := opts.ValidateOptions() + assert.Equal(t, "base fee is lower than the minimum network fee", err.Error()) + }) +} + +func TestSignAndBuildNewFeeBumpTransaction(t *testing.T) { + distributionAccountSignatureClient := signing.SignatureClientMock{} + defer distributionAccountSignatureClient.AssertExpectations(t) + channelAccountSignatureClient := signing.SignatureClientMock{} + defer channelAccountSignatureClient.AssertExpectations(t) + horizonClient := horizonclient.MockClient{} + defer horizonClient.AssertExpectations(t) + txService, _ := NewTransactionService(TransactionServiceOptions{ + DistributionAccountSignatureClient: &distributionAccountSignatureClient, + ChannelAccountSignatureClient: &channelAccountSignatureClient, + HorizonClient: &horizonClient, + BaseFee: 114, + }) + + txStr, _ := utils.BuildTestTransaction().Base64() + + t.Run("malformed_transaction_string", func(t *testing.T) { + feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), "abcd") + assert.Empty(t, feeBumpTx) + assert.ErrorIs(t, tsserror.OriginalXDRMalformed, err) + }) + + t.Run("channel_account_signature_client_get_account_public_key_err", func(t *testing.T) { + channelAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return("", errors.New("channel accounts unavailable")). + Once() + + feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) + assert.Empty(t, feeBumpTx) + assert.Equal(t, "getting channel account public key: channel accounts unavailable", err.Error()) + }) + + t.Run("horizon_client_get_account_detail_err", func(t *testing.T) { + channelAccount := keypair.MustRandom() + channelAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return(channelAccount.Address(), nil). + Once() + + horizonClient. + On("AccountDetail", horizonclient.AccountRequest{ + AccountID: channelAccount.Address(), + }). + Return(horizon.Account{}, errors.New("horizon down")). + Once() + + feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) + assert.Empty(t, feeBumpTx) + assert.Equal(t, "getting channel account details from horizon: horizon down", err.Error()) + }) + + t.Run("horizon_client_sign_stellar_transaction_w_channel_account_err", func(t *testing.T) { + channelAccount := keypair.MustRandom() + channelAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return(channelAccount.Address(), nil). + Once(). + On("SignStellarTransaction", context.Background(), mock.AnythingOfType("*txnbuild.Transaction"), []string{channelAccount.Address()}). + Return(nil, errors.New("unable to sign")). + Once() + + horizonClient. + On("AccountDetail", horizonclient.AccountRequest{ + AccountID: channelAccount.Address(), + }). + Return(horizon.Account{AccountID: channelAccount.Address(), Sequence: 1}, nil). + Once() + + feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) + assert.Empty(t, feeBumpTx) + assert.Equal(t, "signing transaction with channel account: unable to sign", err.Error()) + }) + + t.Run("distribution_account_signature_client_get_account_public_key_err", func(t *testing.T) { + channelAccount := keypair.MustRandom() + signedTx := txnbuild.Transaction{} + channelAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return(channelAccount.Address(), nil). + Once(). + On("SignStellarTransaction", context.Background(), mock.AnythingOfType("*txnbuild.Transaction"), []string{channelAccount.Address()}). + Return(&signedTx, nil). + Once() + + distributionAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return("", errors.New("client down")). + Once() + + horizonClient. + On("AccountDetail", horizonclient.AccountRequest{ + AccountID: channelAccount.Address(), + }). + Return(horizon.Account{AccountID: channelAccount.Address(), Sequence: 1}, nil). + Once() + + feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) + assert.Empty(t, feeBumpTx) + assert.Equal(t, "getting distribution account public key: client down", err.Error()) + }) + + t.Run("horizon_client_sign_stellar_transaction_w_distribition_account_err", func(t *testing.T) { + account := keypair.MustRandom() + signedTx := utils.BuildTestTransaction() + channelAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return(account.Address(), nil). + Once(). + On("SignStellarTransaction", context.Background(), mock.AnythingOfType("*txnbuild.Transaction"), []string{account.Address()}). + Return(signedTx, nil). + Once() + + distributionAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return(account.Address(), nil). + Once(). + On("SignStellarFeeBumpTransaction", context.Background(), mock.AnythingOfType("*txnbuild.FeeBumpTransaction")). + Return(nil, errors.New("unable to sign")). + Once() + + horizonClient. + On("AccountDetail", horizonclient.AccountRequest{ + AccountID: account.Address(), + }). + Return(horizon.Account{AccountID: account.Address(), Sequence: 1}, nil). + Once() + + feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) + assert.Empty(t, feeBumpTx) + assert.Equal(t, "signing the fee bump transaction with distribution account: unable to sign", err.Error()) + }) + + t.Run("returns_signed_tx", func(t *testing.T) { + account := keypair.MustRandom() + signedTx := utils.BuildTestTransaction() + testFeeBumpTx, _ := txnbuild.NewFeeBumpTransaction( + txnbuild.FeeBumpTransactionParams{ + Inner: signedTx, + FeeAccount: account.Address(), + BaseFee: int64(100), + }, + ) + channelAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return(account.Address(), nil). + Once(). + On("SignStellarTransaction", context.Background(), mock.AnythingOfType("*txnbuild.Transaction"), []string{account.Address()}). + Return(signedTx, nil). + Once() + + distributionAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return(account.Address(), nil). + Once(). + On("SignStellarFeeBumpTransaction", context.Background(), mock.AnythingOfType("*txnbuild.FeeBumpTransaction")). + Return(testFeeBumpTx, nil). + Once() + + horizonClient. + On("AccountDetail", horizonclient.AccountRequest{ + AccountID: account.Address(), + }). + Return(horizon.Account{AccountID: account.Address(), Sequence: 1}, nil). + Once() + + feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) + assert.Equal(t, feeBumpTx, testFeeBumpTx) + assert.Empty(t, err) + }) +} diff --git a/internal/tss/services/types.go b/internal/tss/services/types.go deleted file mode 100644 index f395190..0000000 --- a/internal/tss/services/types.go +++ /dev/null @@ -1,7 +0,0 @@ -package services - -import "github.com/stellar/wallet-backend/internal/tss" - -type Service interface { - ProcessPayload(payload tss.Payload) -} diff --git a/internal/tss/services/webhook_handler_service.go b/internal/tss/services/webhook_handler_service.go deleted file mode 100644 index 33c837e..0000000 --- a/internal/tss/services/webhook_handler_service.go +++ /dev/null @@ -1,19 +0,0 @@ -package services - -import ( - "github.com/stellar/wallet-backend/internal/tss" -) - -type webhookHandlerService struct { - channel tss.Channel -} - -func NewWebhookHandlerService(channel tss.Channel) Service { - return &webhookHandlerService{ - channel: channel, - } -} - -func (p *webhookHandlerService) ProcessPayload(payload tss.Payload) { - // fill in later -} diff --git a/internal/tss/store/store.go b/internal/tss/store/store.go index 866f1cc..b7953b0 100644 --- a/internal/tss/store/store.go +++ b/internal/tss/store/store.go @@ -38,7 +38,7 @@ func (s *store) UpsertTransaction(ctx context.Context, webhookURL string, txHash current_status = $4, updated_at = NOW(); ` - _, err := s.DB.ExecContext(ctx, q, txHash, txXDR, webhookURL, string(status)) + _, err := s.DB.ExecContext(ctx, q, txHash, txXDR, webhookURL, status.Status()) if err != nil { return fmt.Errorf("inserting/updatig tss transaction: %w", err) } diff --git a/internal/tss/store/store_test.go b/internal/tss/store/store_test.go index 57709ab..2987a27 100644 --- a/internal/tss/store/store_test.go +++ b/internal/tss/store/store_test.go @@ -7,6 +7,7 @@ import ( "github.com/stellar/go/xdr" "github.com/stellar/wallet-backend/internal/db" "github.com/stellar/wallet-backend/internal/db/dbtest" + "github.com/stellar/wallet-backend/internal/entities" "github.com/stellar/wallet-backend/internal/tss" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -20,7 +21,7 @@ func TestUpsertTransaction(t *testing.T) { defer dbConnectionPool.Close() store := NewStore(dbConnectionPool) t.Run("insert", func(t *testing.T) { - _ = store.UpsertTransaction(context.Background(), "www.stellar.org", "hash", "xdr", tss.NewStatus) + _ = store.UpsertTransaction(context.Background(), "www.stellar.org", "hash", "xdr", tss.RPCTXStatus{OtherStatus: tss.NewStatus}) var status string err = dbConnectionPool.GetContext(context.Background(), &status, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, "hash") @@ -29,13 +30,13 @@ func TestUpsertTransaction(t *testing.T) { }) t.Run("update", func(t *testing.T) { - _ = store.UpsertTransaction(context.Background(), "www.stellar.org", "hash", "xdr", tss.NewStatus) - _ = store.UpsertTransaction(context.Background(), "www.stellar.org", "hash", "xdr", tss.SuccessStatus) + _ = store.UpsertTransaction(context.Background(), "www.stellar.org", "hash", "xdr", tss.RPCTXStatus{OtherStatus: tss.NewStatus}) + _ = store.UpsertTransaction(context.Background(), "www.stellar.org", "hash", "xdr", tss.RPCTXStatus{RPCStatus: entities.SuccessStatus}) var status string err = dbConnectionPool.GetContext(context.Background(), &status, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, "hash") require.NoError(t, err) - assert.Equal(t, status, string(tss.SuccessStatus)) + assert.Equal(t, status, string(entities.SuccessStatus)) var numRows int err = dbConnectionPool.GetContext(context.Background(), &numRows, `SELECT count(*) FROM tss_transactions WHERE transaction_hash = $1`, "hash") diff --git a/internal/tss/types.go b/internal/tss/types.go index a0aafcb..6836a70 100644 --- a/internal/tss/types.go +++ b/internal/tss/types.go @@ -1,19 +1,85 @@ package tss -import "github.com/stellar/go/xdr" +import ( + "bytes" + "encoding/base64" + "fmt" + "strconv" + + xdr3 "github.com/stellar/go-xdr/xdr3" + "github.com/stellar/go/xdr" + "github.com/stellar/wallet-backend/internal/entities" +) + +type RPCGetIngestTxResponse struct { + // A status that indicated whether this transaction failed or successly made it to the ledger + Status entities.RPCStatus + // The error code that is derived by deserialzing the ResultXdr string in the sendTransaction response + // list of possible errror codes: https://developers.stellar.org/docs/data/horizon/api-reference/errors/result-codes/transactions + Code RPCTXCode + // The raw TransactionEnvelope XDR for this transaction + EnvelopeXDR string + // The raw TransactionResult XDR of the envelopeXdr + ResultXDR string + // The unix timestamp of when the transaction was included in the ledger + CreatedAt int64 +} + +//nolint:unused +func ParseToRPCGetIngestTxResponse(result entities.RPCGetTransactionResult, err error) (RPCGetIngestTxResponse, error) { + if err != nil { + return RPCGetIngestTxResponse{Status: entities.ErrorStatus}, err + } + + getIngestTxResponse := RPCGetIngestTxResponse{ + Status: result.Status, + EnvelopeXDR: result.EnvelopeXDR, + ResultXDR: result.ResultXDR, + } + if getIngestTxResponse.Status != entities.NotFoundStatus { + getIngestTxResponse.CreatedAt, err = strconv.ParseInt(result.CreatedAt, 10, 64) + if err != nil { + return RPCGetIngestTxResponse{Status: entities.ErrorStatus}, fmt.Errorf("unable to parse createdAt: %w", err) + } + } + getIngestTxResponse.Code, err = parseSendTransactionErrorXDR(result.ResultXDR) + if err != nil { + return getIngestTxResponse, fmt.Errorf("parse error result xdr string: %w", err) + } + return getIngestTxResponse, nil +} + +type OtherStatus string -type RPCTXStatus string type OtherCodes int32 type TransactionResultCode int32 +const ( + NewStatus OtherStatus = "NEW" + NoStatus OtherStatus = "" +) + +type RPCTXStatus struct { + RPCStatus entities.RPCStatus + OtherStatus OtherStatus +} + +func (s RPCTXStatus) Status() string { + if s.OtherStatus != NoStatus { + return string(s.OtherStatus) + } + return string(s.RPCStatus) +} + const ( // Do not use NoCode NoCode OtherCodes = 0 // These values need to not overlap the values in xdr.TransactionResultCode NewCode OtherCodes = 100 RPCFailCode OtherCodes = 101 - UnMarshalBinaryCode OtherCodes = 102 + UnmarshalBinaryCode OtherCodes = 102 + EmptyCode OtherCodes = 103 ) type RPCTXCode struct { @@ -28,19 +94,14 @@ func (c RPCTXCode) Code() int { return int(c.TxResultCode) } -const ( - // Brand new transaction, not sent to RPC yet - NewStatus RPCTXStatus = "NEW" - // RPC sendTransaction statuses - PendingStatus RPCTXStatus = "PENDING" - DuplicateStatus RPCTXStatus = "DUPLICATE" - TryAgainLaterStatus RPCTXStatus = "TRY_AGAIN_LATER" - ErrorStatus RPCTXStatus = "ERROR" - // RPC getTransaction(s) statuses - NotFoundStatus RPCTXStatus = "NOT_FOUND" - FailedStatus RPCTXStatus = "FAILED" - SuccessStatus RPCTXStatus = "SUCCESS" -) +var FinalErrorCodes = []xdr.TransactionResultCode{ + xdr.TransactionResultCodeTxSuccess, + xdr.TransactionResultCodeTxFailed, + xdr.TransactionResultCodeTxMissingOperation, + xdr.TransactionResultCodeTxInsufficientBalance, + xdr.TransactionResultCodeTxBadAuthExtra, + xdr.TransactionResultCodeTxMalformed, +} var NonJitterErrorCodes = []xdr.TransactionResultCode{ xdr.TransactionResultCodeTxTooEarly, @@ -53,20 +114,6 @@ var JitterErrorCodes = []xdr.TransactionResultCode{ xdr.TransactionResultCodeTxInternalError, } -type RPCGetIngestTxResponse struct { - // A status that indicated whether this transaction failed or successly made it to the ledger - Status RPCTXStatus - // The error code that is derived by deserialzing the ResultXdr string in the sendTransaction response - // list of possible errror codes: https://developers.stellar.org/docs/data/horizon/api-reference/errors/result-codes/transactions - Code RPCTXCode - // The raw TransactionEnvelope XDR for this transaction - EnvelopeXDR string - // The raw TransactionResult XDR of the envelopeXdr - ResultXDR string - // The unix timestamp of when the transaction was included in the ledger - CreatedAt int64 -} - type RPCSendTxResponse struct { // The hash of the transaction submitted to RPC TransactionHash string @@ -78,29 +125,42 @@ type RPCSendTxResponse struct { Code RPCTXCode } -type Payload struct { - WebhookURL string - // The hash of the transaction xdr submitted by the client - the id of the transaction submitted by a client - TransactionHash string - // The xdr of the transaction - TransactionXDR string - // Relevant fields in an RPC sendTransaction response - RpcSubmitTxResponse RPCSendTxResponse - // Relevant fields in the transaction list inside the RPC getTransactions response - RpcGetIngestTxResponse RPCGetIngestTxResponse -} - -type RPCResult struct { - Status string `json:"status"` - EnvelopeXDR string `json:"envelopeXdr"` - ResultXDR string `json:"resultXdr"` - ErrorResultXDR string `json:"errorResultXdr"` - Hash string `json:"hash"` - CreatedAt string `json:"createdAt"` +func ParseToRPCSendTxResponse(transactionXDR string, result entities.RPCSendTransactionResult, err error) (RPCSendTxResponse, error) { + sendTxResponse := RPCSendTxResponse{} + sendTxResponse.TransactionXDR = transactionXDR + if err != nil { + sendTxResponse.Status.RPCStatus = entities.ErrorStatus + sendTxResponse.Code.OtherCodes = RPCFailCode + return sendTxResponse, fmt.Errorf("RPC fail: %w", err) + } + sendTxResponse.Status.RPCStatus = result.Status + sendTxResponse.TransactionHash = result.Hash + sendTxResponse.Code, err = parseSendTransactionErrorXDR(result.ErrorResultXDR) + if err != nil { + return sendTxResponse, fmt.Errorf("parse error result xdr string: %w", err) + } + return sendTxResponse, nil } -type RPCResponse struct { - RPCResult `json:"result"` +func parseSendTransactionErrorXDR(errorResultXDR string) (RPCTXCode, error) { + if errorResultXDR == "" { + return RPCTXCode{ + OtherCodes: EmptyCode, + }, nil + } + unmarshalErr := "unable to unmarshal errorResultXDR: %s" + decodedBytes, err := base64.StdEncoding.DecodeString(errorResultXDR) + if err != nil { + return RPCTXCode{OtherCodes: UnmarshalBinaryCode}, fmt.Errorf(unmarshalErr, errorResultXDR) + } + var errorResult xdr.TransactionResult + _, err = xdr3.Unmarshal(bytes.NewReader(decodedBytes), &errorResult) + if err != nil { + return RPCTXCode{OtherCodes: UnmarshalBinaryCode}, fmt.Errorf(unmarshalErr, errorResultXDR) + } + return RPCTXCode{ + TxResultCode: errorResult.Result.Code, + }, nil } type TSSResponse struct { @@ -112,6 +172,18 @@ type TSSResponse struct { ResultXDR string `json:"resultXdr"` } +type Payload struct { + WebhookURL string + // The hash of the transaction xdr submitted by the client - the id of the transaction submitted by a client + TransactionHash string + // The xdr of the transaction + TransactionXDR string + // Relevant fields in an RPC sendTransaction response + RpcSubmitTxResponse RPCSendTxResponse + // Relevant fields in the transaction list inside the RPC getTransactions response + RpcGetIngestTxResponse RPCGetIngestTxResponse +} + type Channel interface { Send(payload Payload) Receive(payload Payload) diff --git a/internal/tss/types_new.go b/internal/tss/types_new.go new file mode 100644 index 0000000..e69de29 diff --git a/internal/tss/types_old.go b/internal/tss/types_old.go new file mode 100644 index 0000000..cf42dda --- /dev/null +++ b/internal/tss/types_old.go @@ -0,0 +1,121 @@ +package tss + +/* +import "github.com/stellar/go/xdr" + +type RPCTXStatus string +type OtherCodes int32 + +type TransactionResultCode int32 + +const ( + // Do not use NoCode + NoCode OtherCodes = 0 + // These values need to not overlap the values in xdr.TransactionResultCode + NewCode OtherCodes = 100 + RPCFailCode OtherCodes = 101 + UnMarshalBinaryCode OtherCodes = 102 +) + +type RPCTXCode struct { + TxResultCode xdr.TransactionResultCode + OtherCodes OtherCodes +} + +func (c RPCTXCode) Code() int { + if c.OtherCodes != NoCode { + return int(c.OtherCodes) + } + return int(c.TxResultCode) +} + +const ( + // Brand new transaction, not sent to RPC yet + NewStatus RPCTXStatus = "NEW" + // RPC sendTransaction statuses + PendingStatus RPCTXStatus = "PENDING" + DuplicateStatus RPCTXStatus = "DUPLICATE" + TryAgainLaterStatus RPCTXStatus = "TRY_AGAIN_LATER" + ErrorStatus RPCTXStatus = "ERROR" + // RPC getTransaction(s) statuses + NotFoundStatus RPCTXStatus = "NOT_FOUND" + FailedStatus RPCTXStatus = "FAILED" + SuccessStatus RPCTXStatus = "SUCCESS" +) + +var NonJitterErrorCodes = []xdr.TransactionResultCode{ + xdr.TransactionResultCodeTxTooEarly, + xdr.TransactionResultCodeTxTooLate, + xdr.TransactionResultCodeTxBadSeq, +} + +var JitterErrorCodes = []xdr.TransactionResultCode{ + xdr.TransactionResultCodeTxInsufficientFee, + xdr.TransactionResultCodeTxInternalError, +} + +type RPCGetIngestTxResponse struct { + // A status that indicated whether this transaction failed or successly made it to the ledger + Status RPCTXStatus + // The error code that is derived by deserialzing the ResultXdr string in the sendTransaction response + // list of possible errror codes: https://developers.stellar.org/docs/data/horizon/api-reference/errors/result-codes/transactions + Code RPCTXCode + // The raw TransactionEnvelope XDR for this transaction + EnvelopeXDR string + // The raw TransactionResult XDR of the envelopeXdr + ResultXDR string + // The unix timestamp of when the transaction was included in the ledger + CreatedAt int64 +} + +type RPCSendTxResponse struct { + // The hash of the transaction submitted to RPC + TransactionHash string + TransactionXDR string + // The status of an RPC sendTransaction call. Can be one of [PENDING, DUPLICATE, TRY_AGAIN_LATER, ERROR] + Status RPCTXStatus + // The (optional) error code that is derived by deserialzing the errorResultXdr string in the sendTransaction response + // list of possible errror codes: https://developers.stellar.org/docs/data/horizon/api-reference/errors/result-codes/transactions + Code RPCTXCode +} + +type Payload struct { + WebhookURL string + // The hash of the transaction xdr submitted by the client - the id of the transaction submitted by a client + TransactionHash string + // The xdr of the transaction + TransactionXDR string + // Relevant fields in an RPC sendTransaction response + RpcSubmitTxResponse RPCSendTxResponse + // Relevant fields in the transaction list inside the RPC getTransactions response + RpcGetIngestTxResponse RPCGetIngestTxResponse +} + +type RPCResult struct { + Status string `json:"status"` + EnvelopeXDR string `json:"envelopeXdr"` + ResultXDR string `json:"resultXdr"` + ErrorResultXDR string `json:"errorResultXdr"` + Hash string `json:"hash"` + CreatedAt string `json:"createdAt"` +} + +type RPCResponse struct { + RPCResult `json:"result"` +} + +type TSSResponse struct { + TransactionHash string `json:"tx_hash"` + TransactionResultCode string `json:"tx_result_code"` + Status string `json:"status"` + CreatedAt int64 `json:"created_at"` + EnvelopeXDR string `json:"envelopeXdr"` + ResultXDR string `json:"resultXdr"` +} + +type Channel interface { + Send(payload Payload) + Receive(payload Payload) + Stop() +} +*/ diff --git a/internal/tss/types_test.go b/internal/tss/types_test.go new file mode 100644 index 0000000..10b67d1 --- /dev/null +++ b/internal/tss/types_test.go @@ -0,0 +1,92 @@ +package tss + +import ( + "errors" + "testing" + + "github.com/stellar/go/xdr" + "github.com/stellar/wallet-backend/internal/entities" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestParseToRPCSendTxResponse(t *testing.T) { + t.Run("rpc_request_fails", func(t *testing.T) { + resp, err := ParseToRPCGetIngestTxResponse(entities.RPCGetTransactionResult{}, errors.New("sending sendTransaction request: sending POST request to RPC: connection failed")) + require.Error(t, err) + + assert.Equal(t, entities.ErrorStatus, resp.Status) + assert.Equal(t, "sending sendTransaction request: sending POST request to RPC: connection failed", err.Error()) + }) + + t.Run("response_has_empty_errorResultXdr", func(t *testing.T) { + resp, err := ParseToRPCSendTxResponse("", entities.RPCSendTransactionResult{ + Status: "PENDING", + ErrorResultXDR: "", + }, nil) + + assert.Equal(t, entities.PendingStatus, resp.Status.RPCStatus) + assert.Equal(t, EmptyCode, resp.Code.OtherCodes) + assert.Empty(t, err) + }) + + t.Run("response_has_unparsable_errorResultXdr", func(t *testing.T) { + resp, err := ParseToRPCSendTxResponse("", entities.RPCSendTransactionResult{ + ErrorResultXDR: "ABC123", + }, nil) + + assert.Equal(t, UnmarshalBinaryCode, resp.Code.OtherCodes) + assert.Equal(t, "parse error result xdr string: unable to unmarshal errorResultXDR: ABC123", err.Error()) + }) + + t.Run("response_has_errorResultXdr", func(t *testing.T) { + resp, err := ParseToRPCSendTxResponse("", entities.RPCSendTransactionResult{ + ErrorResultXDR: "AAAAAAAAAMj////9AAAAAA==", + }, nil) + + assert.Equal(t, xdr.TransactionResultCodeTxTooLate, resp.Code.TxResultCode) + assert.Empty(t, err) + }) +} + +func TestParseToRPCGetIngestTxResponse(t *testing.T) { + t.Run("rpc_request_fails", func(t *testing.T) { + resp, err := ParseToRPCGetIngestTxResponse(entities.RPCGetTransactionResult{}, errors.New("sending getTransaction request: sending POST request to RPC: connection failed")) + require.Error(t, err) + + assert.Equal(t, entities.ErrorStatus, resp.Status) + assert.Equal(t, "sending getTransaction request: sending POST request to RPC: connection failed", err.Error()) + }) + + t.Run("unable_to_parse_createdAt", func(t *testing.T) { + resp, err := ParseToRPCGetIngestTxResponse(entities.RPCGetTransactionResult{ + Status: "SUCCESS", + CreatedAt: "ABCD", + }, nil) + require.Error(t, err) + + assert.Equal(t, entities.ErrorStatus, resp.Status) + assert.Equal(t, "unable to parse createdAt: strconv.ParseInt: parsing \"ABCD\": invalid syntax", err.Error()) + }) + + t.Run("response_has_createdAt_field", func(t *testing.T) { + resp, err := ParseToRPCGetIngestTxResponse(entities.RPCGetTransactionResult{ + CreatedAt: "1234567", + }, nil) + require.NoError(t, err) + + assert.Equal(t, int64(1234567), resp.CreatedAt) + assert.Empty(t, err) + }) + + t.Run("response_has_errorResultXdr", func(t *testing.T) { + resp, err := ParseToRPCGetIngestTxResponse(entities.RPCGetTransactionResult{ + Status: entities.ErrorStatus, + CreatedAt: "1234567", + ResultXDR: "AAAAAAAAAMj////9AAAAAA==", + }, nil) + + assert.Equal(t, xdr.TransactionResultCodeTxTooLate, resp.Code.TxResultCode) + assert.Empty(t, err) + }) +} diff --git a/internal/tss/utils/helpers.go b/internal/tss/utils/helpers.go index 81cb448..dec166c 100644 --- a/internal/tss/utils/helpers.go +++ b/internal/tss/utils/helpers.go @@ -9,8 +9,8 @@ import ( func PayloadTOTSSResponse(payload tss.Payload) tss.TSSResponse { response := tss.TSSResponse{} response.TransactionHash = payload.TransactionHash - if payload.RpcSubmitTxResponse.Status != "" { - response.Status = string(payload.RpcSubmitTxResponse.Status) + if payload.RpcSubmitTxResponse.Status.Status() != "" { + response.Status = string(payload.RpcSubmitTxResponse.Status.Status()) response.TransactionResultCode = payload.RpcSubmitTxResponse.Code.TxResultCode.String() response.EnvelopeXDR = payload.RpcSubmitTxResponse.TransactionXDR } else if payload.RpcGetIngestTxResponse.Status != "" { diff --git a/internal/tss/utils/mocks.go b/internal/tss/utils/mocks.go deleted file mode 100644 index 597a6c9..0000000 --- a/internal/tss/utils/mocks.go +++ /dev/null @@ -1,50 +0,0 @@ -package utils - -import ( - "context" - "io" - "net/http" - - "github.com/stellar/go/txnbuild" - "github.com/stellar/wallet-backend/internal/tss" - "github.com/stretchr/testify/mock" -) - -type MockHTTPClient struct { - mock.Mock -} - -func (s *MockHTTPClient) Post(url, contentType string, body io.Reader) (resp *http.Response, err error) { - args := s.Called(url, contentType, body) - return args.Get(0).(*http.Response), args.Error(1) -} - -type TransactionServiceMock struct { - mock.Mock -} - -var _ TransactionService = (*TransactionServiceMock)(nil) - -func (t *TransactionServiceMock) NetworkPassphrase() string { - args := t.Called() - return args.String(0) -} - -func (t *TransactionServiceMock) SignAndBuildNewFeeBumpTransaction(ctx context.Context, origTxXdr string) (*txnbuild.FeeBumpTransaction, error) { - args := t.Called(ctx, origTxXdr) - if result := args.Get(0); result != nil { - return result.(*txnbuild.FeeBumpTransaction), args.Error(1) - } - return nil, args.Error(1) - -} - -func (t *TransactionServiceMock) SendTransaction(transactionXdr string) (tss.RPCSendTxResponse, error) { - args := t.Called(transactionXdr) - return args.Get(0).(tss.RPCSendTxResponse), args.Error(1) -} - -func (t *TransactionServiceMock) GetTransaction(transactionHash string) (tss.RPCGetIngestTxResponse, error) { - args := t.Called(transactionHash) - return args.Get(0).(tss.RPCGetIngestTxResponse), args.Error(1) -} diff --git a/internal/tss/utils/transaction_builder.go b/internal/tss/utils/transaction_builder.go index a6dc192..8958313 100644 --- a/internal/tss/utils/transaction_builder.go +++ b/internal/tss/utils/transaction_builder.go @@ -37,13 +37,15 @@ func BuildOriginalTransaction(txOpXDRs []string) (*txnbuild.Transaction, error) operations = append(operations, &paymentOp) } - tx, _ := txnbuild.NewTransaction(txnbuild.TransactionParams{ + tx, err := txnbuild.NewTransaction(txnbuild.TransactionParams{ SourceAccount: &txnbuild.SimpleAccount{ AccountID: keypair.MustRandom().Address(), }, Operations: operations, - BaseFee: 104, Preconditions: txnbuild.Preconditions{TimeBounds: txnbuild.NewTimeout(10)}, }) + if err != nil { + return nil, fmt.Errorf("cannot create new transaction: %w", err) + } return tx, nil } diff --git a/internal/tss/utils/transaction_service_test.go b/internal/tss/utils/transaction_service_test.go deleted file mode 100644 index 3ef6372..0000000 --- a/internal/tss/utils/transaction_service_test.go +++ /dev/null @@ -1,648 +0,0 @@ -package utils - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "strings" - "testing" - - "github.com/stellar/go/clients/horizonclient" - "github.com/stellar/go/keypair" - "github.com/stellar/go/protocols/horizon" - "github.com/stellar/go/txnbuild" - "github.com/stellar/go/xdr" - "github.com/stellar/wallet-backend/internal/signing" - "github.com/stellar/wallet-backend/internal/tss" - tsserror "github.com/stellar/wallet-backend/internal/tss/errors" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" -) - -func TestValidateOptions(t *testing.T) { - t.Run("return_error_when_distribution_signature_client_nil", func(t *testing.T) { - opts := TransactionServiceOptions{ - DistributionAccountSignatureClient: nil, - ChannelAccountSignatureClient: &signing.SignatureClientMock{}, - HorizonClient: &horizonclient.MockClient{}, - RPCURL: "http://localhost:8000/soroban/rpc", - BaseFee: 114, - HTTPClient: &MockHTTPClient{}, - } - err := opts.ValidateOptions() - assert.Equal(t, "distribution account signature client cannot be nil", err.Error()) - - }) - - t.Run("return_error_when_channel_signature_client_nil", func(t *testing.T) { - opts := TransactionServiceOptions{ - DistributionAccountSignatureClient: &signing.SignatureClientMock{}, - ChannelAccountSignatureClient: nil, - HorizonClient: &horizonclient.MockClient{}, - RPCURL: "http://localhost:8000/soroban/rpc", - BaseFee: 114, - HTTPClient: &MockHTTPClient{}, - } - err := opts.ValidateOptions() - assert.Equal(t, "channel account signature client cannot be nil", err.Error()) - }) - - t.Run("return_error_when_horizon_client_nil", func(t *testing.T) { - opts := TransactionServiceOptions{ - DistributionAccountSignatureClient: &signing.SignatureClientMock{}, - ChannelAccountSignatureClient: &signing.SignatureClientMock{}, - HorizonClient: nil, - RPCURL: "http://localhost:8000/soroban/rpc", - BaseFee: 114, - HTTPClient: &MockHTTPClient{}, - } - err := opts.ValidateOptions() - assert.Equal(t, "horizon client cannot be nil", err.Error()) - }) - - t.Run("return_error_when_rpc_url_empty", func(t *testing.T) { - opts := TransactionServiceOptions{ - DistributionAccountSignatureClient: &signing.SignatureClientMock{}, - ChannelAccountSignatureClient: &signing.SignatureClientMock{}, - HorizonClient: &horizonclient.MockClient{}, - RPCURL: "", - BaseFee: 114, - HTTPClient: &MockHTTPClient{}, - } - err := opts.ValidateOptions() - assert.Equal(t, "rpc url cannot be empty", err.Error()) - }) - - t.Run("return_error_when_base_fee_too_low", func(t *testing.T) { - opts := TransactionServiceOptions{ - DistributionAccountSignatureClient: &signing.SignatureClientMock{}, - ChannelAccountSignatureClient: &signing.SignatureClientMock{}, - HorizonClient: &horizonclient.MockClient{}, - RPCURL: "http://localhost:8000/soroban/rpc", - BaseFee: txnbuild.MinBaseFee - 10, - HTTPClient: &MockHTTPClient{}, - } - err := opts.ValidateOptions() - assert.Equal(t, "base fee is lower than the minimum network fee", err.Error()) - }) - - t.Run("return_error_http_client_nil", func(t *testing.T) { - opts := TransactionServiceOptions{ - DistributionAccountSignatureClient: &signing.SignatureClientMock{}, - ChannelAccountSignatureClient: &signing.SignatureClientMock{}, - HorizonClient: &horizonclient.MockClient{}, - RPCURL: "http://localhost:8000/soroban/rpc", - BaseFee: 114, - } - err := opts.ValidateOptions() - assert.Equal(t, "http client cannot be nil", err.Error()) - }) -} - -func TestSignAndBuildNewFeeBumpTransaction(t *testing.T) { - distributionAccountSignatureClient := signing.SignatureClientMock{} - defer distributionAccountSignatureClient.AssertExpectations(t) - channelAccountSignatureClient := signing.SignatureClientMock{} - defer channelAccountSignatureClient.AssertExpectations(t) - horizonClient := horizonclient.MockClient{} - defer horizonClient.AssertExpectations(t) - txService, _ := NewTransactionService(TransactionServiceOptions{ - DistributionAccountSignatureClient: &distributionAccountSignatureClient, - ChannelAccountSignatureClient: &channelAccountSignatureClient, - HorizonClient: &horizonClient, - RPCURL: "http://localhost:8000/soroban/rpc", - BaseFee: 114, - HTTPClient: &MockHTTPClient{}, - }) - - txStr, _ := BuildTestTransaction().Base64() - - t.Run("malformed_transaction_string", func(t *testing.T) { - feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), "abcd") - assert.Empty(t, feeBumpTx) - assert.ErrorIs(t, tsserror.OriginalXDRMalformed, err) - }) - - t.Run("channel_account_signature_client_get_account_public_key_err", func(t *testing.T) { - channelAccountSignatureClient. - On("GetAccountPublicKey", context.Background()). - Return("", errors.New("channel accounts unavailable")). - Once() - - feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) - assert.Empty(t, feeBumpTx) - assert.Equal(t, "getting channel account public key: channel accounts unavailable", err.Error()) - }) - - t.Run("horizon_client_get_account_detail_err", func(t *testing.T) { - channelAccount := keypair.MustRandom() - channelAccountSignatureClient. - On("GetAccountPublicKey", context.Background()). - Return(channelAccount.Address(), nil). - Once() - - horizonClient. - On("AccountDetail", horizonclient.AccountRequest{ - AccountID: channelAccount.Address(), - }). - Return(horizon.Account{}, errors.New("horizon down")). - Once() - - feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) - assert.Empty(t, feeBumpTx) - assert.Equal(t, "getting channel account details from horizon: horizon down", err.Error()) - }) - - t.Run("horizon_client_sign_stellar_transaction_w_channel_account_err", func(t *testing.T) { - channelAccount := keypair.MustRandom() - channelAccountSignatureClient. - On("GetAccountPublicKey", context.Background()). - Return(channelAccount.Address(), nil). - Once(). - On("SignStellarTransaction", context.Background(), mock.AnythingOfType("*txnbuild.Transaction"), []string{channelAccount.Address()}). - Return(nil, errors.New("unable to sign")). - Once() - - horizonClient. - On("AccountDetail", horizonclient.AccountRequest{ - AccountID: channelAccount.Address(), - }). - Return(horizon.Account{AccountID: channelAccount.Address(), Sequence: 1}, nil). - Once() - - feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) - assert.Empty(t, feeBumpTx) - assert.Equal(t, "signing transaction with channel account: unable to sign", err.Error()) - }) - - t.Run("distribution_account_signature_client_get_account_public_key_err", func(t *testing.T) { - channelAccount := keypair.MustRandom() - signedTx := txnbuild.Transaction{} - channelAccountSignatureClient. - On("GetAccountPublicKey", context.Background()). - Return(channelAccount.Address(), nil). - Once(). - On("SignStellarTransaction", context.Background(), mock.AnythingOfType("*txnbuild.Transaction"), []string{channelAccount.Address()}). - Return(&signedTx, nil). - Once() - - distributionAccountSignatureClient. - On("GetAccountPublicKey", context.Background()). - Return("", errors.New("client down")). - Once() - - horizonClient. - On("AccountDetail", horizonclient.AccountRequest{ - AccountID: channelAccount.Address(), - }). - Return(horizon.Account{AccountID: channelAccount.Address(), Sequence: 1}, nil). - Once() - - feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) - assert.Empty(t, feeBumpTx) - assert.Equal(t, "getting distribution account public key: client down", err.Error()) - }) - - t.Run("horizon_client_sign_stellar_transaction_w_distribition_account_err", func(t *testing.T) { - account := keypair.MustRandom() - signedTx := BuildTestTransaction() - channelAccountSignatureClient. - On("GetAccountPublicKey", context.Background()). - Return(account.Address(), nil). - Once(). - On("SignStellarTransaction", context.Background(), mock.AnythingOfType("*txnbuild.Transaction"), []string{account.Address()}). - Return(signedTx, nil). - Once() - - distributionAccountSignatureClient. - On("GetAccountPublicKey", context.Background()). - Return(account.Address(), nil). - Once(). - On("SignStellarFeeBumpTransaction", context.Background(), mock.AnythingOfType("*txnbuild.FeeBumpTransaction")). - Return(nil, errors.New("unable to sign")). - Once() - - horizonClient. - On("AccountDetail", horizonclient.AccountRequest{ - AccountID: account.Address(), - }). - Return(horizon.Account{AccountID: account.Address(), Sequence: 1}, nil). - Once() - - feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) - assert.Empty(t, feeBumpTx) - assert.Equal(t, "signing the fee bump transaction with distribution account: unable to sign", err.Error()) - }) - - t.Run("returns_signed_tx", func(t *testing.T) { - account := keypair.MustRandom() - signedTx := BuildTestTransaction() - testFeeBumpTx, _ := txnbuild.NewFeeBumpTransaction( - txnbuild.FeeBumpTransactionParams{ - Inner: signedTx, - FeeAccount: account.Address(), - BaseFee: int64(100), - }, - ) - channelAccountSignatureClient. - On("GetAccountPublicKey", context.Background()). - Return(account.Address(), nil). - Once(). - On("SignStellarTransaction", context.Background(), mock.AnythingOfType("*txnbuild.Transaction"), []string{account.Address()}). - Return(signedTx, nil). - Once() - - distributionAccountSignatureClient. - On("GetAccountPublicKey", context.Background()). - Return(account.Address(), nil). - Once(). - On("SignStellarFeeBumpTransaction", context.Background(), mock.AnythingOfType("*txnbuild.FeeBumpTransaction")). - Return(testFeeBumpTx, nil). - Once() - - horizonClient. - On("AccountDetail", horizonclient.AccountRequest{ - AccountID: account.Address(), - }). - Return(horizon.Account{AccountID: account.Address(), Sequence: 1}, nil). - Once() - - feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) - assert.Equal(t, feeBumpTx, testFeeBumpTx) - assert.Empty(t, err) - }) -} - -func TestParseErrorResultXDR(t *testing.T) { - distributionAccountSignatureClient := signing.SignatureClientMock{} - defer distributionAccountSignatureClient.AssertExpectations(t) - channelAccountSignatureClient := signing.SignatureClientMock{} - defer channelAccountSignatureClient.AssertExpectations(t) - horizonClient := horizonclient.MockClient{} - defer horizonClient.AssertExpectations(t) - txService, _ := NewTransactionService(TransactionServiceOptions{ - DistributionAccountSignatureClient: &distributionAccountSignatureClient, - ChannelAccountSignatureClient: &channelAccountSignatureClient, - HorizonClient: &horizonClient, - RPCURL: "http://localhost:8000/soroban/rpc", - BaseFee: 114, - HTTPClient: &MockHTTPClient{}, - }) - - t.Run("errorResultXdr_empty", func(t *testing.T) { - _, err := txService.parseErrorResultXDR("") - assert.Equal(t, "unable to unmarshal errorResultXdr: ", err.Error()) - }) - - t.Run("errorResultXdr_invalid", func(t *testing.T) { - _, err := txService.parseErrorResultXDR("ABCD") - assert.Equal(t, "unable to unmarshal errorResultXdr: ABCD", err.Error()) - }) - - t.Run("errorResultXdr_valid", func(t *testing.T) { - resp, err := txService.parseErrorResultXDR("AAAAAAAAAMj////9AAAAAA==") - assert.Equal(t, xdr.TransactionResultCodeTxTooLate, resp.TxResultCode) - assert.Empty(t, err) - }) -} - -type errorReader struct{} - -func (e *errorReader) Read(p []byte) (n int, err error) { - return 0, fmt.Errorf("read error") -} - -func (e *errorReader) Close() error { - return nil -} - -func TestSendRPCRequest(t *testing.T) { - mockHTTPClient := MockHTTPClient{} - rpcURL := "http://localhost:8000/soroban/rpc" - txService, _ := NewTransactionService(TransactionServiceOptions{ - DistributionAccountSignatureClient: &signing.SignatureClientMock{}, - ChannelAccountSignatureClient: &signing.SignatureClientMock{}, - HorizonClient: &horizonclient.MockClient{}, - RPCURL: rpcURL, - BaseFee: 114, - HTTPClient: &mockHTTPClient, - }) - method := "sendTransaction" - params := map[string]string{"transaction": "ABCD"} - payload := map[string]interface{}{ - "jsonrpc": "2.0", - "id": 1, - "method": method, - "params": params, - } - jsonData, _ := json.Marshal(payload) - t.Run("rpc_post_call_fails", func(t *testing.T) { - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(&http.Response{}, errors.New("RPC Connection fail")). - Once() - - resp, err := txService.sendRPCRequest(method, params) - - assert.Empty(t, resp) - assert.Equal(t, "sendTransaction: sending POST request to rpc: RPC Connection fail", err.Error()) - }) - - t.Run("unmarshaling_rpc_response_fails", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(&errorReader{}), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.sendRPCRequest(method, params) - - assert.Empty(t, resp) - assert.Equal(t, "sendTransaction: unmarshaling RPC response", err.Error()) - }) - - t.Run("unmarshaling_json_fails", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{invalid-json`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.sendRPCRequest(method, params) - - assert.Empty(t, resp) - assert.Equal(t, "sendTransaction: parsing RPC response JSON", err.Error()) - }) - - t.Run("response_has_no_result_field", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"status": "success"}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, _ := txService.sendRPCRequest(method, params) - assert.Empty(t, resp) - }) - - t.Run("response_has_status_field", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"status": "PENDING"}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.sendRPCRequest(method, params) - - assert.Equal(t, "PENDING", resp.Status) - assert.Empty(t, err) - }) - - t.Run("response_has_envelopexdr_field", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"envelopeXdr": "exdr"}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.sendRPCRequest(method, params) - - assert.Equal(t, "exdr", resp.EnvelopeXDR) - assert.Empty(t, err) - }) - - t.Run("response_has_resultxdr_field", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"resultXdr": "rxdr"}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.sendRPCRequest(method, params) - - assert.Equal(t, "rxdr", resp.ResultXDR) - assert.Empty(t, err) - }) - - t.Run("response_has_errorresultxdr_field", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"errorResultXdr": "exdr"}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.sendRPCRequest(method, params) - - assert.Equal(t, "exdr", resp.ErrorResultXDR) - assert.Empty(t, err) - }) - - t.Run("response_has_hash_field", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"hash": "hash"}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.sendRPCRequest(method, params) - - assert.Equal(t, "hash", resp.Hash) - assert.Empty(t, err) - }) - - t.Run("response_has_createdat_field", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"createdAt": "1234"}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.sendRPCRequest(method, params) - - assert.Equal(t, "1234", resp.CreatedAt) - assert.Empty(t, err) - }) -} - -func TestSendTransaction(t *testing.T) { - mockHTTPClient := MockHTTPClient{} - rpcURL := "http://localhost:8000/soroban/rpc" - txService, _ := NewTransactionService(TransactionServiceOptions{ - DistributionAccountSignatureClient: &signing.SignatureClientMock{}, - ChannelAccountSignatureClient: &signing.SignatureClientMock{}, - HorizonClient: &horizonclient.MockClient{}, - RPCURL: rpcURL, - BaseFee: 114, - HTTPClient: &mockHTTPClient, - }) - method := "sendTransaction" - params := map[string]string{"transaction": "ABCD"} - payload := map[string]interface{}{ - "jsonrpc": "2.0", - "id": 1, - "method": method, - "params": params, - } - jsonData, _ := json.Marshal(payload) - - t.Run("rpc_request_fails", func(t *testing.T) { - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(&http.Response{}, errors.New("RPC Connection fail")). - Once() - - resp, err := txService.SendTransaction("ABCD") - - assert.Equal(t, tss.ErrorStatus, resp.Status) - assert.Equal(t, tss.RPCFailCode, resp.Code.OtherCodes) - assert.Equal(t, "RPC fail: sendTransaction: sending POST request to rpc: RPC Connection fail", err.Error()) - - }) - t.Run("response_has_unparsable_errorResultXdr", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"status": "ERROR", "errorResultXdr": "ABC123"}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.SendTransaction("ABCD") - - assert.Equal(t, tss.ErrorStatus, resp.Status) - assert.Equal(t, tss.UnMarshalBinaryCode, resp.Code.OtherCodes) - assert.Equal(t, "unable to unmarshal errorResultXdr: ABC123", err.Error()) - }) - t.Run("response_has_empty_errorResultXdr_wth_status", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"status": "PENDING", "errorResultXdr": ""}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.SendTransaction("ABCD") - - assert.Equal(t, tss.PendingStatus, resp.Status) - assert.Equal(t, tss.UnMarshalBinaryCode, resp.Code.OtherCodes) - assert.Equal(t, "unable to unmarshal errorResultXdr: ", err.Error()) - }) - t.Run("response_has_errorResultXdr", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"status": "ERROR", "errorResultXdr": "AAAAAAAAAMj////9AAAAAA=="}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.SendTransaction("ABCD") - - assert.Equal(t, tss.ErrorStatus, resp.Status) - assert.Equal(t, xdr.TransactionResultCodeTxTooLate, resp.Code.TxResultCode) - assert.Empty(t, err) - }) -} - -func TestGetTransaction(t *testing.T) { - mockHTTPClient := MockHTTPClient{} - rpcURL := "http://localhost:8000/soroban/rpc" - txService, _ := NewTransactionService(TransactionServiceOptions{ - DistributionAccountSignatureClient: &signing.SignatureClientMock{}, - ChannelAccountSignatureClient: &signing.SignatureClientMock{}, - HorizonClient: &horizonclient.MockClient{}, - RPCURL: rpcURL, - BaseFee: 114, - HTTPClient: &mockHTTPClient, - }) - method := "getTransaction" - params := map[string]string{"hash": "XYZ"} - payload := map[string]interface{}{ - "jsonrpc": "2.0", - "id": 1, - "method": method, - "params": params, - } - jsonData, _ := json.Marshal(payload) - - t.Run("rpc_request_fails", func(t *testing.T) { - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(&http.Response{}, errors.New("RPC Connection fail")). - Once() - - resp, err := txService.GetTransaction("XYZ") - - assert.Equal(t, tss.ErrorStatus, resp.Status) - assert.Equal(t, "RPC Fail: getTransaction: sending POST request to rpc: RPC Connection fail", err.Error()) - - }) - t.Run("unable_to_parse_createdAt", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"status": "SUCCESS", "createdAt": "ABCD"}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.GetTransaction("XYZ") - - assert.Equal(t, tss.ErrorStatus, resp.Status) - assert.Equal(t, "unable to parse createAt: strconv.ParseInt: parsing \"ABCD\": invalid syntax", err.Error()) - }) - t.Run("response_has_createdAt_resultXdr_field", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"status": "FAILED", "resultXdr": "AAAAAAAAAMj////9AAAAAA==", "createdAt": "1234567"}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.GetTransaction("XYZ") - - assert.Equal(t, tss.FailedStatus, resp.Status) - assert.Equal(t, xdr.TransactionResultCodeTxTooLate, resp.Code.TxResultCode) - assert.Equal(t, int64(1234567), resp.CreatedAt) - assert.Empty(t, err) - }) - -} diff --git a/internal/utils/http_client.go b/internal/utils/http_client.go new file mode 100644 index 0000000..514abf0 --- /dev/null +++ b/internal/utils/http_client.go @@ -0,0 +1,21 @@ +package utils + +import ( + "io" + "net/http" + + "github.com/stretchr/testify/mock" +) + +type HTTPClient interface { + Post(url string, t string, body io.Reader) (resp *http.Response, err error) +} + +type MockHTTPClient struct { + mock.Mock +} + +func (s *MockHTTPClient) Post(url, contentType string, body io.Reader) (resp *http.Response, err error) { + args := s.Called(url, contentType, body) + return args.Get(0).(*http.Response), args.Error(1) +} From 4e324a6bdb80b853f6eccc934b0ad14377d9cee0 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Thu, 26 Sep 2024 14:44:15 -0700 Subject: [PATCH 076/113] merging in changes from eror_handler_service --- cmd/serve.go | 1 - internal/serve/serve.go | 16 +++++++++++++++- internal/tss/channels/webhook_channel.go | 16 ++++++++-------- internal/tss/channels/webhook_channel_test.go | 4 ++-- internal/tss/types_new.go | 0 5 files changed, 25 insertions(+), 12 deletions(-) delete mode 100644 internal/tss/types_new.go diff --git a/cmd/serve.go b/cmd/serve.go index 7e7ecf7..0d6e855 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -35,7 +35,6 @@ func (c *serveCmd) Command() *cobra.Command { utils.ChannelAccountEncryptionPassphraseOption(&cfg.EncryptionPassphrase), utils.SentryDSNOption(&sentryDSN), utils.StellarEnvironmentOption(&stellarEnvironment), - utils.RPCURLOption(&cfg.RPCURL), utils.ErrorHandlerServiceJitterChannelBufferSizeOption(&cfg.ErrorHandlerServiceJitterChannelBufferSize), utils.ErrorHandlerServiceJitterChannelMaxWorkersOption(&cfg.ErrorHandlerServiceJitterChannelMaxWorkers), utils.ErrorHandlerServiceNonJitterChannelBufferSizeOption(&cfg.ErrorHandlerServiceNonJitterChannelBufferSize), diff --git a/internal/serve/serve.go b/internal/serve/serve.go index 4a0050d..8a7b79a 100644 --- a/internal/serve/serve.go +++ b/internal/serve/serve.go @@ -98,6 +98,7 @@ type handlerDeps struct { RPCCallerChannel tss.Channel ErrorJitterChannel tss.Channel ErrorNonJitterChannel tss.Channel + WebhookChannel tss.Channel TSSRouter tssrouter.Router AppTracker apptracker.AppTracker } @@ -121,6 +122,7 @@ func Serve(cfg Configs) error { deps.ErrorJitterChannel.Stop() deps.ErrorNonJitterChannel.Stop() deps.RPCCallerChannel.Stop() + deps.WebhookChannel.Stop() }, }) @@ -235,11 +237,22 @@ func initHandlerDeps(cfg Configs) (handlerDeps, error) { errorNonJitterChannel := tsschannel.NewErrorNonJitterChannel(errorNonJitterChannelConfigs) + httpClient = http.Client{Timeout: time.Duration(30 * time.Second)} + webhookChannelConfigs := tsschannel.WebhookChannelConfigs{ + HTTPClient: &httpClient, + MaxBufferSize: cfg.WebhookHandlerServiceChannelMaxBufferSize, + MaxWorkers: cfg.WebhookHandlerServiceChannelMaxWorkers, + MaxRetries: cfg.WebhookHandlerServiceChannelMaxRetries, + MinWaitBtwnRetriesMS: cfg.WebhookHandlerServiceChannelMinWaitBtwnRetriesMS, + } + + webhookChannel := tsschannel.NewWebhookChannel(webhookChannelConfigs) + router := tssrouter.NewRouter(tssrouter.RouterConfigs{ RPCCallerChannel: rpcCallerChannel, ErrorJitterChannel: errorJitterChannel, ErrorNonJitterChannel: errorNonJitterChannel, - WebhookChannel: nil, + WebhookChannel: webhookChannel, }) rpcCallerChannel.SetRouter(router) @@ -258,6 +271,7 @@ func initHandlerDeps(cfg Configs) (handlerDeps, error) { RPCCallerChannel: rpcCallerChannel, ErrorJitterChannel: errorJitterChannel, ErrorNonJitterChannel: errorNonJitterChannel, + WebhookChannel: webhookChannel, TSSRouter: router, }, nil } diff --git a/internal/tss/channels/webhook_channel.go b/internal/tss/channels/webhook_channel.go index c005a51..31efae5 100644 --- a/internal/tss/channels/webhook_channel.go +++ b/internal/tss/channels/webhook_channel.go @@ -13,7 +13,7 @@ import ( "github.com/stellar/wallet-backend/internal/utils" ) -type WebhookHandlerServiceChannelConfigs struct { +type WebhookChannelConfigs struct { HTTPClient utils.HTTPClient MaxBufferSize int MaxWorkers int @@ -21,18 +21,18 @@ type WebhookHandlerServiceChannelConfigs struct { MinWaitBtwnRetriesMS int } -type webhookHandlerServicePool struct { +type webhookPool struct { Pool *pond.WorkerPool HTTPClient utils.HTTPClient MaxRetries int MinWaitBtwnRetriesMS int } -var _ tss.Channel = (*webhookHandlerServicePool)(nil) +var _ tss.Channel = (*webhookPool)(nil) -func NewWebhookHandlerServiceChannel(cfg WebhookHandlerServiceChannelConfigs) *webhookHandlerServicePool { +func NewWebhookChannel(cfg WebhookChannelConfigs) *webhookPool { pool := pond.New(cfg.MaxBufferSize, cfg.MaxWorkers, pond.Strategy(pond.Balanced())) - return &webhookHandlerServicePool{ + return &webhookPool{ Pool: pool, HTTPClient: cfg.HTTPClient, MaxRetries: cfg.MaxRetries, @@ -41,13 +41,13 @@ func NewWebhookHandlerServiceChannel(cfg WebhookHandlerServiceChannelConfigs) *w } -func (p *webhookHandlerServicePool) Send(payload tss.Payload) { +func (p *webhookPool) Send(payload tss.Payload) { p.Pool.Submit(func() { p.Receive(payload) }) } -func (p *webhookHandlerServicePool) Receive(payload tss.Payload) { +func (p *webhookPool) Receive(payload tss.Payload) { resp := tssutils.PayloadTOTSSResponse(payload) jsonData, err := json.Marshal(resp) if err != nil { @@ -70,6 +70,6 @@ func (p *webhookHandlerServicePool) Receive(payload tss.Payload) { } } -func (p *webhookHandlerServicePool) Stop() { +func (p *webhookPool) Stop() { p.Pool.StopAndWait() } diff --git a/internal/tss/channels/webhook_channel_test.go b/internal/tss/channels/webhook_channel_test.go index 56d3235..88e4cf3 100644 --- a/internal/tss/channels/webhook_channel_test.go +++ b/internal/tss/channels/webhook_channel_test.go @@ -15,14 +15,14 @@ import ( func TestWebhookHandlerServiceChannel(t *testing.T) { mockHTTPClient := utils.MockHTTPClient{} - cfg := WebhookHandlerServiceChannelConfigs{ + cfg := WebhookChannelConfigs{ HTTPClient: &mockHTTPClient, MaxBufferSize: 1, MaxWorkers: 1, MaxRetries: 3, MinWaitBtwnRetriesMS: 5, } - channel := NewWebhookHandlerServiceChannel(cfg) + channel := NewWebhookChannel(cfg) payload := tss.Payload{} payload.WebhookURL = "www.stellar.org" diff --git a/internal/tss/types_new.go b/internal/tss/types_new.go deleted file mode 100644 index e69de29..0000000 From 4fff0393708bbc56c396843b81b73645119a79d4 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Thu, 26 Sep 2024 14:45:22 -0700 Subject: [PATCH 077/113] delete file --- internal/tss/types_old.go | 121 -------------------------------------- 1 file changed, 121 deletions(-) delete mode 100644 internal/tss/types_old.go diff --git a/internal/tss/types_old.go b/internal/tss/types_old.go deleted file mode 100644 index cf42dda..0000000 --- a/internal/tss/types_old.go +++ /dev/null @@ -1,121 +0,0 @@ -package tss - -/* -import "github.com/stellar/go/xdr" - -type RPCTXStatus string -type OtherCodes int32 - -type TransactionResultCode int32 - -const ( - // Do not use NoCode - NoCode OtherCodes = 0 - // These values need to not overlap the values in xdr.TransactionResultCode - NewCode OtherCodes = 100 - RPCFailCode OtherCodes = 101 - UnMarshalBinaryCode OtherCodes = 102 -) - -type RPCTXCode struct { - TxResultCode xdr.TransactionResultCode - OtherCodes OtherCodes -} - -func (c RPCTXCode) Code() int { - if c.OtherCodes != NoCode { - return int(c.OtherCodes) - } - return int(c.TxResultCode) -} - -const ( - // Brand new transaction, not sent to RPC yet - NewStatus RPCTXStatus = "NEW" - // RPC sendTransaction statuses - PendingStatus RPCTXStatus = "PENDING" - DuplicateStatus RPCTXStatus = "DUPLICATE" - TryAgainLaterStatus RPCTXStatus = "TRY_AGAIN_LATER" - ErrorStatus RPCTXStatus = "ERROR" - // RPC getTransaction(s) statuses - NotFoundStatus RPCTXStatus = "NOT_FOUND" - FailedStatus RPCTXStatus = "FAILED" - SuccessStatus RPCTXStatus = "SUCCESS" -) - -var NonJitterErrorCodes = []xdr.TransactionResultCode{ - xdr.TransactionResultCodeTxTooEarly, - xdr.TransactionResultCodeTxTooLate, - xdr.TransactionResultCodeTxBadSeq, -} - -var JitterErrorCodes = []xdr.TransactionResultCode{ - xdr.TransactionResultCodeTxInsufficientFee, - xdr.TransactionResultCodeTxInternalError, -} - -type RPCGetIngestTxResponse struct { - // A status that indicated whether this transaction failed or successly made it to the ledger - Status RPCTXStatus - // The error code that is derived by deserialzing the ResultXdr string in the sendTransaction response - // list of possible errror codes: https://developers.stellar.org/docs/data/horizon/api-reference/errors/result-codes/transactions - Code RPCTXCode - // The raw TransactionEnvelope XDR for this transaction - EnvelopeXDR string - // The raw TransactionResult XDR of the envelopeXdr - ResultXDR string - // The unix timestamp of when the transaction was included in the ledger - CreatedAt int64 -} - -type RPCSendTxResponse struct { - // The hash of the transaction submitted to RPC - TransactionHash string - TransactionXDR string - // The status of an RPC sendTransaction call. Can be one of [PENDING, DUPLICATE, TRY_AGAIN_LATER, ERROR] - Status RPCTXStatus - // The (optional) error code that is derived by deserialzing the errorResultXdr string in the sendTransaction response - // list of possible errror codes: https://developers.stellar.org/docs/data/horizon/api-reference/errors/result-codes/transactions - Code RPCTXCode -} - -type Payload struct { - WebhookURL string - // The hash of the transaction xdr submitted by the client - the id of the transaction submitted by a client - TransactionHash string - // The xdr of the transaction - TransactionXDR string - // Relevant fields in an RPC sendTransaction response - RpcSubmitTxResponse RPCSendTxResponse - // Relevant fields in the transaction list inside the RPC getTransactions response - RpcGetIngestTxResponse RPCGetIngestTxResponse -} - -type RPCResult struct { - Status string `json:"status"` - EnvelopeXDR string `json:"envelopeXdr"` - ResultXDR string `json:"resultXdr"` - ErrorResultXDR string `json:"errorResultXdr"` - Hash string `json:"hash"` - CreatedAt string `json:"createdAt"` -} - -type RPCResponse struct { - RPCResult `json:"result"` -} - -type TSSResponse struct { - TransactionHash string `json:"tx_hash"` - TransactionResultCode string `json:"tx_result_code"` - Status string `json:"status"` - CreatedAt int64 `json:"created_at"` - EnvelopeXDR string `json:"envelopeXdr"` - ResultXDR string `json:"resultXdr"` -} - -type Channel interface { - Send(payload Payload) - Receive(payload Payload) - Stop() -} -*/ From f4aaf93b934a82afc7c155a1ce24786162983e14 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Thu, 26 Sep 2024 18:15:33 -0700 Subject: [PATCH 078/113] commit current changes before merging branch --- internal/entities/rpc.go | 91 ++++++ internal/ingest/ingest.go | 53 ++++ internal/services/ingest.go | 144 +++++++++ internal/services/rpc_service.go | 133 +++++++++ internal/services/rpc_service_test.go | 282 ++++++++++++++++++ .../servicesmocks/rpc_service_mocks.go | 27 ++ ...handler_service_non_jitter_channel_test.go | 224 -------------- ...ter_channel.go => error_jitter_channel.go} | 53 ++-- .../tss/channels/error_jitter_channel_test.go | 141 +++++++++ ...channel.go => error_non_jitter_channel.go} | 47 +-- .../channels/error_non_jitter_channel_test.go | 140 +++++++++ ...ror_service_handler_jitter_channel_test.go | 224 -------------- internal/tss/channels/rpc_caller_channel.go | 81 +++++ .../tss/channels/rpc_caller_channel_test.go | 130 ++++++++ .../channels/rpc_caller_service_channel.go | 78 ----- .../rpc_caller_service_channel_test.go | 207 ------------- internal/tss/channels/utils.go | 54 ---- ..._service_channel.go => webhook_channel.go} | 21 +- ...hannel_test.go => webhook_channel_test.go} | 9 +- internal/tss/router/mocks.go | 5 +- internal/tss/router/router.go | 79 ++--- internal/tss/router/router_test.go | 130 ++++++-- .../tss/services/error_handler_service.go | 36 --- .../services/error_handler_service_test.go | 54 ---- internal/tss/services/mocks.go | 43 ++- internal/tss/services/rpc_caller_service.go | 21 -- internal/tss/services/transaction_manager.go | 76 +++++ .../transaction_manager_test.go} | 108 +++++-- internal/tss/services/transaction_service.go | 127 ++++++++ .../tss/services/transaction_service_test.go | 236 +++++++++++++++ internal/tss/services/types.go | 7 - .../tss/services/webhook_handler_service.go | 19 -- internal/tss/store/store.go | 2 +- internal/tss/store/store_test.go | 9 +- internal/tss/types.go | 174 +++++++---- internal/tss/types_curr.go | 148 +++++++++ internal/tss/types_test.go | 92 ++++++ internal/tss/utils/helpers.go | 4 +- internal/tss/utils/mocks.go | 50 ---- internal/tss/utils/transaction_service.go | 72 ++++- .../tss/utils/transaction_service_test.go | 89 +----- internal/utils/http_client.go | 21 ++ internal/utils/ingestion_utils.go | 15 + 43 files changed, 2481 insertions(+), 1275 deletions(-) create mode 100644 internal/entities/rpc.go create mode 100644 internal/services/rpc_service.go create mode 100644 internal/services/rpc_service_test.go create mode 100644 internal/services/servicesmocks/rpc_service_mocks.go delete mode 100644 internal/tss/channels/error_handler_service_non_jitter_channel_test.go rename internal/tss/channels/{error_handler_service_jitter_channel.go => error_jitter_channel.go} (51%) create mode 100644 internal/tss/channels/error_jitter_channel_test.go rename internal/tss/channels/{error_handler_service_non_jitter_channel.go => error_non_jitter_channel.go} (53%) create mode 100644 internal/tss/channels/error_non_jitter_channel_test.go delete mode 100644 internal/tss/channels/error_service_handler_jitter_channel_test.go create mode 100644 internal/tss/channels/rpc_caller_channel.go create mode 100644 internal/tss/channels/rpc_caller_channel_test.go delete mode 100644 internal/tss/channels/rpc_caller_service_channel.go delete mode 100644 internal/tss/channels/rpc_caller_service_channel_test.go delete mode 100644 internal/tss/channels/utils.go rename internal/tss/channels/{webhook_handler_service_channel.go => webhook_channel.go} (71%) rename internal/tss/channels/{webhook_handler_service_channel_test.go => webhook_channel_test.go} (81%) delete mode 100644 internal/tss/services/error_handler_service.go delete mode 100644 internal/tss/services/error_handler_service_test.go delete mode 100644 internal/tss/services/rpc_caller_service.go create mode 100644 internal/tss/services/transaction_manager.go rename internal/tss/{channels/utils_test.go => services/transaction_manager_test.go} (50%) create mode 100644 internal/tss/services/transaction_service.go create mode 100644 internal/tss/services/transaction_service_test.go delete mode 100644 internal/tss/services/types.go delete mode 100644 internal/tss/services/webhook_handler_service.go create mode 100644 internal/tss/types_curr.go create mode 100644 internal/tss/types_test.go delete mode 100644 internal/tss/utils/mocks.go create mode 100644 internal/utils/http_client.go diff --git a/internal/entities/rpc.go b/internal/entities/rpc.go new file mode 100644 index 0000000..9e83c08 --- /dev/null +++ b/internal/entities/rpc.go @@ -0,0 +1,91 @@ +package entities + +import ( + "encoding/json" +) + +type RPCStatus string + +const ( + // sendTransaction statuses + PendingStatus RPCStatus = "PENDING" + DuplicateStatus RPCStatus = "DUPLICATE" + TryAgainLaterStatus RPCStatus = "TRY_AGAIN_LATER" + ErrorStatus RPCStatus = "ERROR" + // getTransaction statuses + NotFoundStatus RPCStatus = "NOT_FOUND" + FailedStatus RPCStatus = "FAILED" + SuccessStatus RPCStatus = "SUCCESS" +) + +type RPCEntry struct { + Key string `json:"key"` + XDR string `json:"xdr"` + LastModifiedLedgerSeq int64 `json:"lastModifiedLedgerSeq"` +} + +type RPCResponse struct { + Result json.RawMessage `json:"result"` + JSONRPC string `json:"jsonrpc"` + ID int64 `json:"id"` +} + +type RPCGetLedgerEntriesResult struct { + Entries []RPCEntry `json:"entries"` +} + +type RPCGetTransactionResult struct { + Status RPCStatus `json:"status"` + LatestLedger int64 `json:"latestLedger"` + LatestLedgerCloseTime string `json:"latestLedgerCloseTime"` + OldestLedger int64 `json:"oldestLedger"` + OldestLedgerCloseTime string `json:"oldestLedgerCloseTime"` + ApplicationOrder int64 `json:"applicationOrder"` + EnvelopeXDR string `json:"envelopeXdr"` + ResultXDR string `json:"resultXdr"` + ResultMetaXDR string `json:"resultMetaXdr"` + Ledger int64 `json:"ledger"` + CreatedAt string `json:"createdAt"` + ErrorResultXDR string `json:"errorResultXdr"` +} + +type Transaction struct { + Status RPCStatus `json:"status"` + ApplicationOrder int64 `json:"applicationOrder"` + FeeBump bool `json:"feeBump"` + EnvelopeXDR string `json:"envelopeXdr"` + ResultXDR string `json:"resultXdr"` + ResultMetaXDR string `json:"resultMetaXdr"` + DiagnosticEventsXDR string `json:"diagnosticEventsXdr"` + CreatedAt string `json:"createdAt"` + ErrorResultXDR string `json:"errorResultXdr"` +} + +type RPCGetTransactionsResult struct { + Transactions []Transaction `json:"transactions"` + LatestLedger int64 `json:"latestLedger"` + LatestLedgerCloseTime string `json:"latestLedgerCloseTimestamp"` + OldestLedger int64 `json:"oldestLedger"` + OldestLedgerCloseTime string `json:"oldestLedgerCloseTimestamp"` + Cursor string `json:"cursor"` +} + +type RPCSendTransactionResult struct { + Status RPCStatus `json:"status"` + LatestLedger int64 `json:"latestLedger"` + LatestLedgerCloseTime string `json:"latestLedgerCloseTime"` + Hash string `json:"hash"` + ErrorResultXDR string `json:"errorResultXdr"` +} + +type RPCPagination struct { + Cursor string `json:"cursor,omitempty"` + Limit int `json:"limit"` +} + +type RPCParams struct { + Transaction string `json:"transaction,omitempty"` + Hash string `json:"hash,omitempty"` + StartLedger int `json:"startLedger,omitempty"` + Pagination RPCPagination `json:"pagination,omitempty"` +} diff --git a/internal/ingest/ingest.go b/internal/ingest/ingest.go index 330aa7a..da8f408 100644 --- a/internal/ingest/ingest.go +++ b/internal/ingest/ingest.go @@ -4,8 +4,10 @@ import ( "context" "errors" "fmt" + "net/http" "os" "path" + "time" "github.com/sirupsen/logrus" "github.com/stellar/go/ingest/ledgerbackend" @@ -15,8 +17,22 @@ import ( "github.com/stellar/wallet-backend/internal/data" "github.com/stellar/wallet-backend/internal/db" "github.com/stellar/wallet-backend/internal/services" + tssservices "github.com/stellar/wallet-backend/internal/tss/utils" ) +// Change configs to have router, transactionservice, StartingLedger, store (to look up the transaction xdr/hash to change status), AppTracker + +type RPCConfigs struct { + LedgerCursorName string + RPCCursorName string + StartLedger int + StartCursor string + DatabaseURL string + LogLevel logrus.Level + AppTracker apptracker.AppTracker + RPCURL string +} + type Configs struct { DatabaseURL string NetworkPassphrase string @@ -29,6 +45,21 @@ type Configs struct { AppTracker apptracker.AppTracker } +func RPCIngest(cfg RPCConfigs) error { + ctx := context.Background() + + manager, err := setupRPCDeps(cfg) + if err != nil { + log.Ctx(ctx).Fatalf("Error setting up dependencies for ingest: %v", err) + } + + if err = manager.Run(ctx, uint32(cfg.StartLedger), cfg.StartCursor); err != nil { + log.Ctx(ctx).Fatalf("Running ingest from start ledger: %d, start cursor: %d: %v", cfg.StartLedger, cfg.StartCursor, err) + } + + return nil +} + func Ingest(cfg Configs) error { ctx := context.Background() @@ -44,6 +75,28 @@ func Ingest(cfg Configs) error { return nil } +func setupRPCDeps(cfg RPCConfigs) (*services.RPCIngestManager, error) { + dbConnectionPool, err := db.OpenDBConnectionPool(cfg.DatabaseURL) + if err != nil { + return nil, fmt.Errorf("error connecting to the database: %w", err) + } + models, err := data.NewModels(dbConnectionPool) + if err != nil { + return nil, fmt.Errorf("error creating models for Serve: %w", err) + } + httpClient := http.Client{Timeout: time.Duration(30 * time.Second)} + txServiceOpts := tssservices.TransactionServiceOptions{RPCURL: cfg.RPCURL, HTTPClient: &httpClient} + + txService, err := tssservices.NewTransactionService(txServiceOpts) + return &services.RPCIngestManager{ + PaymentModel: models.Payments, + AppTracker: cfg.AppTracker, + TransactionService: txService, + LedgerCursorName: cfg.LedgerCursorName, + RPCCursorName: cfg.RPCCursorName, + }, nil +} + func setupDeps(cfg Configs) (*services.IngestManager, error) { // Open DB connection pool dbConnectionPool, err := db.OpenDBConnectionPool(cfg.DatabaseURL) diff --git a/internal/services/ingest.go b/internal/services/ingest.go index ee55d37..6b919d3 100644 --- a/internal/services/ingest.go +++ b/internal/services/ingest.go @@ -16,6 +16,14 @@ import ( "github.com/stellar/wallet-backend/internal/utils" ) +type RPCIngestManager struct { + RPCService RPCService + PaymentModel *data.PaymentModel + AppTracker apptracker.AppTracker + LedgerCursorName string + RPCCursorName string +} + type IngestManager struct { PaymentModel *data.PaymentModel LedgerBackend ledgerbackend.LedgerBackend @@ -24,6 +32,51 @@ type IngestManager struct { AppTracker apptracker.AppTracker } +/* +func (m *RPCIngestManager) Run(ctx context.Context, startLedger uint32, startCursor string) error { + heartbeat := make(chan any) + go trackServiceHealth(heartbeat, m.AppTracker) + cursor := "" + ledger := 0 + if startCursor != "" { + cursor = startCursor + } else if startLedger != 0 { + ledger = int(startLedger) + } else { + lastSyncedCursor, err := m.PaymentModel.GetLatestLedgerSynced(ctx, m.RPCCursorName) + if err != nil { + return fmt.Errorf("getting last cursor synced: %w", err) + } + if lastSyncedCursor == 0 { + lastSyncedLedger, err := m.PaymentModel.GetLatestLedgerSynced(ctx, m.LedgerCursorName) + if err != nil { + return fmt.Errorf("getting last ledger synced: %w", err) + } + ledger = int(lastSyncedLedger) + } + cursor = strconv.FormatUint(uint64(lastSyncedCursor), 10) + } + + for { + time.Sleep(10) + txns, cursor, err := m.TransactionService.GetTransactions(ledger, cursor, 200) + if err != nil { + return fmt.Errorf("getTransactions: %w", err) + } + heartbeat <- true + iCursor, err := strconv.ParseUint(cursor, 10, 32) + if err != nil { + return fmt.Errorf("cannot convert cursor to int: %s", err.Error()) + } + err = m.PaymentModel.UpdateLatestLedgerSynced(ctx, m.RPCCursorName, uint32(iCursor)) + if err != nil { + return err + } + m.processGetTransactionsResponse(ctx, txns) + } +} +*/ + func (m *IngestManager) Run(ctx context.Context, start, end uint32) error { var ingestLedger uint32 @@ -121,6 +174,66 @@ func trackServiceHealth(heartbeat chan any, tracker apptracker.AppTracker) { } } +/* +func (m *RPCIngestManager) processGetTransactionsResponse(ctx context.Context, txns []tss.RPCGetIngestTxResponse) (err error) { + return db.RunInTransaction(ctx, m.PaymentModel.DB, nil, func(dbTx db.Transaction) error { + for _, tx := range txns { + if tx.Status != tss.SuccessStatus { + continue + } + + genericTx, err := txnbuild.TransactionFromXDR(tx.EnvelopeXDR) + if err != nil { + return fmt.Errorf("deserializing envelope xdr: %w", err) + } + txEnvelopeXDR, err := genericTx.ToXDR() + if err != nil { + return fmt.Errorf("generic transaction cannot be unpacked into a transaction") + } + txResultXDR, err := m.TransactionService.UnmarshalTransactionResultXDR(tx.ResultXDR) + if err != nil { + return fmt.Errorf("cannot unmarshal transacation result xdr: %s", err.Error()) + } + + txHash := "" + + txMemo, txMemoType := utils.Memo(txEnvelopeXDR.Memo(), "") + if txMemo != nil { + *txMemo = utils.SanitizeUTF8(*txMemo) + } + txEnvelopeXDR.SourceAccount() + for idx, op := range txEnvelopeXDR.Operations() { + opIdx := idx + 1 + + payment := data.Payment{ + OperationID: utils.OperationID(int32(tx.Ledger), int32(tx.ApplicationOrder), int32(opIdx)), + OperationType: op.Body.Type.String(), + TransactionID: utils.TransactionID(int32(tx.Ledger), int32(tx.ApplicationOrder)), + TransactionHash: txHash, + FromAddress: utils.SourceAccountRPC(op, txEnvelopeXDR), + CreatedAt: time.Unix(tx.CreatedAt, 0), + Memo: txMemo, + MemoType: txMemoType, + } + + switch op.Body.Type { + case xdr.OperationTypePayment: + fillPayment(&payment, op.Body) + case xdr.OperationTypePathPaymentStrictSend: + fillPathSendRPC(&payment, op.Body, txResultXDR, opIdx) + case xdr.OperationTypePathPaymentStrictReceive: + fillPathReceiveRPC(&payment, op.Body, txResultXDR, opIdx) + default: + continue + } + } + + } + return nil + }) +} +*/ + func (m *IngestManager) processLedger(ctx context.Context, ledger uint32, ledgerMeta xdr.LedgerCloseMeta) (err error) { reader, err := ingest.NewLedgerTransactionReaderFromLedgerCloseMeta(m.NetworkPassphrase, ledgerMeta) if err != nil { @@ -144,6 +257,9 @@ func (m *IngestManager) processLedger(ctx context.Context, ledger uint32, ledger continue } + // tx = deserialized envelopeXdr tx.Envelope.Memo() is now envelope.Memo() + // tx.Index = applicationOrder - GetTransactions has to return this. May have to update GetTransaction to get this too + // cant I just use the transaction hash here? txHash := utils.TransactionHash(ledgerMeta, int(tx.Index)) txMemo, txMemoType := utils.Memo(tx.Envelope.Memo(), txHash) // The memo field is subject to user input, so we sanitize before persisting in the database @@ -205,6 +321,20 @@ func fillPayment(payment *data.Payment, operation xdr.OperationBody) { payment.DestAmount = payment.SrcAmount } +func fillPathSendRPC(payment *data.Payment, operation xdr.OperationBody, txResult xdr.TransactionResult, operationIdx int) { + pathOp := operation.MustPathPaymentStrictSendOp() + result := utils.OperationResultRPC(txResult, operationIdx).MustPathPaymentStrictSendResult() + payment.ToAddress = pathOp.Destination.Address() + payment.SrcAssetCode = utils.AssetCode(pathOp.SendAsset) + payment.SrcAssetIssuer = pathOp.SendAsset.GetIssuer() + payment.SrcAssetType = pathOp.SendAsset.Type.String() + payment.SrcAmount = int64(pathOp.SendAmount) + payment.DestAssetCode = utils.AssetCode(pathOp.DestAsset) + payment.DestAssetIssuer = pathOp.DestAsset.GetIssuer() + payment.DestAssetType = pathOp.DestAsset.Type.String() + payment.DestAmount = int64(result.DestAmount()) +} + func fillPathSend(payment *data.Payment, operation xdr.OperationBody, transaction ingest.LedgerTransaction, operationIdx int) { pathOp := operation.MustPathPaymentStrictSendOp() result := utils.OperationResult(transaction, operationIdx).MustPathPaymentStrictSendResult() @@ -219,6 +349,20 @@ func fillPathSend(payment *data.Payment, operation xdr.OperationBody, transactio payment.DestAmount = int64(result.DestAmount()) } +func fillPathReceiveRPC(payment *data.Payment, operation xdr.OperationBody, txResult xdr.TransactionResult, operationIdx int) { + pathOp := operation.MustPathPaymentStrictReceiveOp() + result := utils.OperationResultRPC(txResult, operationIdx).MustPathPaymentStrictReceiveResult() + payment.ToAddress = pathOp.Destination.Address() + payment.SrcAssetCode = utils.AssetCode(pathOp.SendAsset) + payment.SrcAssetIssuer = pathOp.SendAsset.GetIssuer() + payment.SrcAssetType = pathOp.SendAsset.Type.String() + payment.SrcAmount = int64(result.SendAmount()) + payment.DestAssetCode = utils.AssetCode(pathOp.DestAsset) + payment.DestAssetIssuer = pathOp.DestAsset.GetIssuer() + payment.DestAssetType = pathOp.DestAsset.Type.String() + payment.DestAmount = int64(pathOp.DestAmount) +} + func fillPathReceive(payment *data.Payment, operation xdr.OperationBody, transaction ingest.LedgerTransaction, operationIdx int) { pathOp := operation.MustPathPaymentStrictReceiveOp() result := utils.OperationResult(transaction, operationIdx).MustPathPaymentStrictReceiveResult() diff --git a/internal/services/rpc_service.go b/internal/services/rpc_service.go new file mode 100644 index 0000000..430e310 --- /dev/null +++ b/internal/services/rpc_service.go @@ -0,0 +1,133 @@ +package services + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + + "github.com/stellar/wallet-backend/internal/entities" + "github.com/stellar/wallet-backend/internal/utils" +) + +type RPCService interface { + GetTransaction(transactionHash string) (entities.RPCGetTransactionResult, error) + SendTransaction(transactionXDR string) (entities.RPCSendTransactionResult, error) +} + +type rpcService struct { + rpcURL string + httpClient utils.HTTPClient +} + +var PageLimit = 200 + +var _ RPCService = (*rpcService)(nil) + +func NewRPCService(rpcURL string, httpClient utils.HTTPClient) (*rpcService, error) { + if rpcURL == "" { + return nil, errors.New("rpcURL cannot be nil") + } + if httpClient == nil { + return nil, errors.New("httpClient cannot be nil") + } + + return &rpcService{ + rpcURL: rpcURL, + httpClient: httpClient, + }, nil +} + +func (r *rpcService) GetTransaction(transactionHash string) (entities.RPCGetTransactionResult, error) { + resultBytes, err := r.sendRPCRequest("getTransaction", entities.RPCParams{Hash: transactionHash}) + if err != nil { + return entities.RPCGetTransactionResult{}, fmt.Errorf("sending getTransaction request: %w", err) + } + + var result entities.RPCGetTransactionResult + err = json.Unmarshal(resultBytes, &result) + if err != nil { + return entities.RPCGetTransactionResult{}, fmt.Errorf("parsing getTransaction result JSON: %w", err) + } + + return result, nil +} + +func (r *rpcService) GetTransactions(startLedger int, startCursor string, limit int) (entities.RPCGetTransactionsResult, error) { + if limit > PageLimit { + return entities.RPCGetTransactionsResult{}, fmt.Errorf("limit cannot exceed") + } + params := entities.RPCParams{} + if startCursor != "" { + pagination := entities.RPCPagination{Cursor: startCursor, Limit: limit} + params.Pagination = pagination + } else { + pagination := entities.RPCPagination{Limit: limit} + params.Pagination = pagination + params.StartLedger = startLedger + } + resultBytes, err := r.sendRPCRequest("getTransactions", params) + if err != nil { + return entities.RPCGetTransactionsResult{}, fmt.Errorf("sending getTransactions request: %w", err) + } + + var result entities.RPCGetTransactionsResult + err = json.Unmarshal(resultBytes, &result) + if err != nil { + return entities.RPCGetTransactionsResult{}, fmt.Errorf("parsing getTransactions result JSON: %w", err) + } + return result, nil +} + +func (r *rpcService) SendTransaction(transactionXDR string) (entities.RPCSendTransactionResult, error) { + + resultBytes, err := r.sendRPCRequest("sendTransaction", entities.RPCParams{Transaction: transactionXDR}) + if err != nil { + return entities.RPCSendTransactionResult{}, fmt.Errorf("sending sendTransaction request: %w", err) + } + + var result entities.RPCSendTransactionResult + err = json.Unmarshal(resultBytes, &result) + if err != nil { + return entities.RPCSendTransactionResult{}, fmt.Errorf("parsing sendTransaction result JSON: %w", err) + } + + return result, nil +} + +func (r *rpcService) sendRPCRequest(method string, params entities.RPCParams) (json.RawMessage, error) { + payload := map[string]interface{}{ + "jsonrpc": "2.0", + "id": 1, + "method": method, + "params": params, + } + jsonData, err := json.Marshal(payload) + if err != nil { + return nil, fmt.Errorf("marshaling payload") + } + + resp, err := r.httpClient.Post(r.rpcURL, "application/json", bytes.NewBuffer(jsonData)) + if err != nil { + return nil, fmt.Errorf("sending POST request to RPC: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("unmarshaling RPC response: %w", err) + } + var res entities.RPCResponse + err = json.Unmarshal(body, &res) + if err != nil { + return nil, fmt.Errorf("parsing RPC response JSON: %w", err) + } + fmt.Println("RPC RESULT") + fmt.Println(res) + if res.Result == nil { + return nil, fmt.Errorf("response %s missing result field", string(body)) + } + + return res.Result, nil +} diff --git a/internal/services/rpc_service_test.go b/internal/services/rpc_service_test.go new file mode 100644 index 0000000..db9c8d7 --- /dev/null +++ b/internal/services/rpc_service_test.go @@ -0,0 +1,282 @@ +package services + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "net/http" + "strings" + "testing" + "time" + + "github.com/stellar/wallet-backend/internal/entities" + "github.com/stellar/wallet-backend/internal/utils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +type errorReader struct{} + +func (e *errorReader) Read(p []byte) (n int, err error) { + return 0, fmt.Errorf("read error") +} + +func (e *errorReader) Close() error { + return nil +} + +func TestRPCCalls(t *testing.T) { + httpClient := http.Client{Timeout: time.Duration(30 * time.Second)} + rpcURL := "http://localhost:8000/soroban/rpc" + rpcService, _ := NewRPCService(rpcURL, &httpClient) + + // SendTransaction + txXDR := "AAAAAgAAAAAVdLFaRzu3r8PAYnF6HoZDvlLId7GDj5q2gfvqMv8GKgAAAGQAAlQIAAAAAQAAAAEAAAAAAAAAAAAAAABm9fgPAAAAAAAAAAEAAAAAAAAAAQAAAACCUVwoK4/wAdfrrDuA0n4x4DVybhDKwSejzetRpCNoFwAAAAAAAAAAAJiWgAAAAAAAAAAA" + resp1, _ := rpcService.SendTransaction(txXDR) + fmt.Println("SEND TX RESPONSE") + fmt.Println(resp1) + //fmt.Println(err) + + // GetTransaction + txHash := "784fec9d8ea31d050874bb09340e662394f618a5391c7aa15f8565756304acc1" + resp2, _ := rpcService.GetTransaction(txHash) + fmt.Println("GET TX RESPONSE") + fmt.Println(resp2) + //fmt.Println(err.Error()) + + // GetTransactions + ledger := 152761 + resp3, err := rpcService.GetTransactions(ledger, "", 50) + fmt.Println("GET TXS RESPONSE") + fmt.Println(resp3) + fmt.Println(err) + +} + +func TestSendRPCRequest(t *testing.T) { + mockHTTPClient := utils.MockHTTPClient{} + rpcURL := "http://api.vibrantapp.com/soroban/rpc" + rpcService, _ := NewRPCService(rpcURL, &mockHTTPClient) + + t.Run("successful", func(t *testing.T) { + httpResponse := http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{ + "jsonrpc": "2.0", + "id": 8675309, + "result": { + "testValue": "theTestValue" + } + }`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", mock.Anything). + Return(&httpResponse, nil). + Once() + + resp, err := rpcService.sendRPCRequest("sendTransaction", entities.RPCParams{}) + require.NoError(t, err) + + var resultStruct struct { + TestValue string `json:"testValue"` + } + err = json.Unmarshal(resp, &resultStruct) + require.NoError(t, err) + + assert.Equal(t, "theTestValue", resultStruct.TestValue) + }) + + t.Run("rpc_post_call_fails", func(t *testing.T) { + mockHTTPClient. + On("Post", rpcURL, "application/json", mock.Anything). + Return(&http.Response{}, errors.New("connection failed")). + Once() + + resp, err := rpcService.sendRPCRequest("sendTransaction", entities.RPCParams{}) + assert.Nil(t, resp) + assert.Equal(t, "sending POST request to RPC: connection failed", err.Error()) + }) + + t.Run("unmarshaling_rpc_response_fails", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(&errorReader{}), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", mock.Anything). + Return(httpResponse, nil). + Once() + + resp, err := rpcService.sendRPCRequest("sendTransaction", entities.RPCParams{}) + assert.Nil(t, resp) + assert.Equal(t, "unmarshaling RPC response: read error", err.Error()) + }) + + t.Run("unmarshaling_json_fails", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{invalid-json`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", mock.Anything). + Return(httpResponse, nil). + Once() + + resp, err := rpcService.sendRPCRequest("sendTransaction", entities.RPCParams{}) + assert.Nil(t, resp) + assert.Equal(t, "parsing RPC response JSON: invalid character 'i' looking for beginning of object key string", err.Error()) + }) + + t.Run("response_has_no_result_field", func(t *testing.T) { + httpResponse := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{"status": "success"}`)), + } + mockHTTPClient. + On("Post", rpcURL, "application/json", mock.Anything). + Return(httpResponse, nil). + Once() + + result, err := rpcService.sendRPCRequest("sendTransaction", entities.RPCParams{}) + assert.Empty(t, result) + assert.Equal(t, `response {"status": "success"} missing result field`, err.Error()) + }) +} + +func TestSendTransaction(t *testing.T) { + mockHTTPClient := utils.MockHTTPClient{} + rpcURL := "http://api.vibrantapp.com/soroban/rpc" + rpcService, _ := NewRPCService(rpcURL, &mockHTTPClient) + + t.Run("successful", func(t *testing.T) { + transactionXDR := "AAAAAgAAAABYJgX6SmA2tGVDv3GXfOWbkeL869ahE0e5DG9HnXQw/QAAAGQAAjpnAAAAAQAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAQAAAACxaDFEbbssZfrbRgFxTYIygITSQxsUpDmneN2gAZBEFQAAAAAAAAAABfXhAAAAAAAAAAAA" + params := entities.RPCParams{Transaction: transactionXDR} + payload := map[string]interface{}{ + "jsonrpc": "2.0", + "id": 1, + "method": "sendTransaction", + "params": params, + } + jsonData, _ := json.Marshal(payload) + + httpResponse := http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{ + "jsonrpc": "2.0", + "id": 8675309, + "result": { + "status": "PENDING", + "hash": "d8ec9b68780314ffdfdfc2194b1b35dd27d7303c3bceaef6447e31631a1419dc", + "latestLedger": 2553978, + "latestLedgerCloseTime": "1700159337" + } + }`)), + } + + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(&httpResponse, nil). + Once() + + result, err := rpcService.SendTransaction(transactionXDR) + require.NoError(t, err) + + assert.Equal(t, entities.RPCSendTransactionResult{ + Status: "PENDING", + Hash: "d8ec9b68780314ffdfdfc2194b1b35dd27d7303c3bceaef6447e31631a1419dc", + LatestLedger: 2553978, + LatestLedgerCloseTime: "1700159337", + }, result) + }) + + t.Run("rpc_request_fails", func(t *testing.T) { + mockHTTPClient. + On("Post", rpcURL, "application/json", mock.Anything). + Return(&http.Response{}, errors.New("connection failed")). + Once() + + result, err := rpcService.SendTransaction("XDR") + require.Error(t, err) + + assert.Equal(t, entities.RPCSendTransactionResult{}, result) + assert.Equal(t, "sending sendTransaction request: sending POST request to RPC: connection failed", err.Error()) + }) +} + +func TestGetTransaction(t *testing.T) { + mockHTTPClient := utils.MockHTTPClient{} + rpcURL := "http://api.vibrantapp.com/soroban/rpc" + rpcService, _ := NewRPCService(rpcURL, &mockHTTPClient) + + t.Run("successful", func(t *testing.T) { + transactionHash := "6bc97bddc21811c626839baf4ab574f4f9f7ddbebb44d286ae504396d4e752da" + params := entities.RPCParams{Hash: transactionHash} + payload := map[string]interface{}{ + "jsonrpc": "2.0", + "id": 1, + "method": "getTransaction", + "params": params, + } + jsonData, _ := json.Marshal(payload) + + httpResponse := http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{ + "jsonrpc": "2.0", + "id": 8675309, + "result": { + "status": "SUCCESS", + "latestLedger": 2540076, + "latestLedgerCloseTime": "1700086333", + "oldestLedger": 2538637, + "oldestLedgerCloseTime": "1700078796", + "applicationOrder": 1, + "envelopeXdr": "AAAAAgAAAADGFY14/R1KD0VGtTbi5Yp4d7LuMW0iQbLM/AUiGKj5owCpsoQAJY3OAAAjqgAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAGAAAAAAAAAABhhOwI+RL18Zpk7cqI5pRRf0L96jE8i+0x3ekhuBh2cUAAAARc2V0X2N1cnJlbmN5X3JhdGUAAAAAAAACAAAADwAAAANldXIAAAAACQAAAAAAAAAAAAAAAAARCz4AAAABAAAAAAAAAAAAAAABhhOwI+RL18Zpk7cqI5pRRf0L96jE8i+0x3ekhuBh2cUAAAARc2V0X2N1cnJlbmN5X3JhdGUAAAAAAAACAAAADwAAAANldXIAAAAACQAAAAAAAAAAAAAAAAARCz4AAAAAAAAAAQAAAAAAAAABAAAAB4408vVXuLU3mry897TfPpYjjsSN7n42REos241RddYdAAAAAQAAAAYAAAABhhOwI+RL18Zpk7cqI5pRRf0L96jE8i+0x3ekhuBh2cUAAAAUAAAAAQFvcYAAAImAAAAHxAAAAAAAAAACAAAAARio+aMAAABATbFMyom/TUz87wHex0LoYZA8jbNJkXbaDSgmOdk+wSBFJuMuta+/vSlro0e0vK2+1FqD/zWHZeYig4pKmM3rDA==", + "resultXdr": "AAAAAAARFy8AAAAAAAAAAQAAAAAAAAAYAAAAAMu8SHUN67hTUJOz3q+IrH9M/4dCVXaljeK6x1Ss20YWAAAAAA==", + "resultMetaXdr": "AAAAAwAAAAAAAAACAAAAAwAmwiAAAAAAAAAAAMYVjXj9HUoPRUa1NuLlinh3su4xbSJBssz8BSIYqPmjAAAAFUHZob0AJY3OAAAjqQAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAMAAAAAACbCHwAAAABlVUH3AAAAAAAAAAEAJsIgAAAAAAAAAADGFY14/R1KD0VGtTbi5Yp4d7LuMW0iQbLM/AUiGKj5owAAABVB2aG9ACWNzgAAI6oAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAADAAAAAAAmwiAAAAAAZVVB/AAAAAAAAAABAAAAAgAAAAMAJsIfAAAABgAAAAAAAAABhhOwI+RL18Zpk7cqI5pRRf0L96jE8i+0x3ekhuBh2cUAAAAUAAAAAQAAABMAAAAAjjTy9Ve4tTeavLz3tN8+liOOxI3ufjZESizbjVF11h0AAAABAAAABQAAABAAAAABAAAAAQAAAA8AAAAJQ29yZVN0YXRlAAAAAAAAEQAAAAEAAAAGAAAADwAAAAVhZG1pbgAAAAAAABIAAAAAAAAAADn1LT+CCK/HiHMChoEi/AtPrkos4XRR2E45Pr25lb3/AAAADwAAAAljb2xfdG9rZW4AAAAAAAASAAAAAdeSi3LCcDzP6vfrn/TvTVBKVai5efybRQ6iyEK00c5hAAAADwAAAAxvcmFjbGVfYWRtaW4AAAASAAAAAAAAAADGFY14/R1KD0VGtTbi5Yp4d7LuMW0iQbLM/AUiGKj5owAAAA8AAAAKcGFuaWNfbW9kZQAAAAAAAAAAAAAAAAAPAAAAEHByb3RvY29sX21hbmFnZXIAAAASAAAAAAAAAAAtSfyAwmj05lZ0WduHsQYQZgvahCNVtZyqS2HRC99kyQAAAA8AAAANc3RhYmxlX2lzc3VlcgAAAAAAABIAAAAAAAAAAEM5BlXva0R5UN6SCMY+6evwJa4mY/f062z0TKLnqN4wAAAAEAAAAAEAAAACAAAADwAAAAhDdXJyZW5jeQAAAA8AAAADZXVyAAAAABEAAAABAAAABQAAAA8AAAAGYWN0aXZlAAAAAAAAAAAAAQAAAA8AAAAIY29udHJhY3QAAAASAAAAAUGpebFxuPbvxZFzOxh8TWAxUwFgraPxPuJEY/8yhiYEAAAADwAAAAxkZW5vbWluYXRpb24AAAAPAAAAA2V1cgAAAAAPAAAAC2xhc3RfdXBkYXRlAAAAAAUAAAAAZVVBvgAAAA8AAAAEcmF0ZQAAAAkAAAAAAAAAAAAAAAAAEQb8AAAAEAAAAAEAAAACAAAADwAAAAhDdXJyZW5jeQAAAA8AAAADdXNkAAAAABEAAAABAAAABQAAAA8AAAAGYWN0aXZlAAAAAAAAAAAAAQAAAA8AAAAIY29udHJhY3QAAAASAAAAATUEqdkvrE2LnSiwOwed3v4VEaulOEiS1rxQw6rJkfxCAAAADwAAAAxkZW5vbWluYXRpb24AAAAPAAAAA3VzZAAAAAAPAAAAC2xhc3RfdXBkYXRlAAAAAAUAAAAAZVVB9wAAAA8AAAAEcmF0ZQAAAAkAAAAAAAAAAAAAAAAAEnzuAAAAEAAAAAEAAAACAAAADwAAAApWYXVsdHNJbmZvAAAAAAAPAAAAA2V1cgAAAAARAAAAAQAAAAgAAAAPAAAADGRlbm9taW5hdGlvbgAAAA8AAAADZXVyAAAAAA8AAAAKbG93ZXN0X2tleQAAAAAAEAAAAAEAAAACAAAADwAAAARTb21lAAAAEQAAAAEAAAADAAAADwAAAAdhY2NvdW50AAAAABIAAAAAAAAAAGKaH7iFUU2kfGOJGONeYuJ2U2QUeQ+zOEfYZvAoeHDsAAAADwAAAAxkZW5vbWluYXRpb24AAAAPAAAAA2V1cgAAAAAPAAAABWluZGV4AAAAAAAACQAAAAAAAAAAAAAAA7msoAAAAAAPAAAADG1pbl9jb2xfcmF0ZQAAAAkAAAAAAAAAAAAAAAAAp9jAAAAADwAAABFtaW5fZGVidF9jcmVhdGlvbgAAAAAAAAkAAAAAAAAAAAAAAAA7msoAAAAADwAAABBvcGVuaW5nX2NvbF9yYXRlAAAACQAAAAAAAAAAAAAAAACveeAAAAAPAAAACXRvdGFsX2NvbAAAAAAAAAkAAAAAAAAAAAAAAAlQL5AAAAAADwAAAAp0b3RhbF9kZWJ0AAAAAAAJAAAAAAAAAAAAAAAAlQL5AAAAAA8AAAAMdG90YWxfdmF1bHRzAAAABQAAAAAAAAABAAAAEAAAAAEAAAACAAAADwAAAApWYXVsdHNJbmZvAAAAAAAPAAAAA3VzZAAAAAARAAAAAQAAAAgAAAAPAAAADGRlbm9taW5hdGlvbgAAAA8AAAADdXNkAAAAAA8AAAAKbG93ZXN0X2tleQAAAAAAEAAAAAEAAAACAAAADwAAAARTb21lAAAAEQAAAAEAAAADAAAADwAAAAdhY2NvdW50AAAAABIAAAAAAAAAAGKaH7iFUU2kfGOJGONeYuJ2U2QUeQ+zOEfYZvAoeHDsAAAADwAAAAxkZW5vbWluYXRpb24AAAAPAAAAA3VzZAAAAAAPAAAABWluZGV4AAAAAAAACQAAAAAAAAAAAAAAA7msoAAAAAAPAAAADG1pbl9jb2xfcmF0ZQAAAAkAAAAAAAAAAAAAAAAAp9jAAAAADwAAABFtaW5fZGVidF9jcmVhdGlvbgAAAAAAAAkAAAAAAAAAAAAAAAA7msoAAAAADwAAABBvcGVuaW5nX2NvbF9yYXRlAAAACQAAAAAAAAAAAAAAAACveeAAAAAPAAAACXRvdGFsX2NvbAAAAAAAAAkAAAAAAAAAAAAAABF2WS4AAAAADwAAAAp0b3RhbF9kZWJ0AAAAAAAJAAAAAAAAAAAAAAAA7msoAAAAAA8AAAAMdG90YWxfdmF1bHRzAAAABQAAAAAAAAACAAAAAAAAAAEAJsIgAAAABgAAAAAAAAABhhOwI+RL18Zpk7cqI5pRRf0L96jE8i+0x3ekhuBh2cUAAAAUAAAAAQAAABMAAAAAjjTy9Ve4tTeavLz3tN8+liOOxI3ufjZESizbjVF11h0AAAABAAAABQAAABAAAAABAAAAAQAAAA8AAAAJQ29yZVN0YXRlAAAAAAAAEQAAAAEAAAAGAAAADwAAAAVhZG1pbgAAAAAAABIAAAAAAAAAADn1LT+CCK/HiHMChoEi/AtPrkos4XRR2E45Pr25lb3/AAAADwAAAAljb2xfdG9rZW4AAAAAAAASAAAAAdeSi3LCcDzP6vfrn/TvTVBKVai5efybRQ6iyEK00c5hAAAADwAAAAxvcmFjbGVfYWRtaW4AAAASAAAAAAAAAADGFY14/R1KD0VGtTbi5Yp4d7LuMW0iQbLM/AUiGKj5owAAAA8AAAAKcGFuaWNfbW9kZQAAAAAAAAAAAAAAAAAPAAAAEHByb3RvY29sX21hbmFnZXIAAAASAAAAAAAAAAAtSfyAwmj05lZ0WduHsQYQZgvahCNVtZyqS2HRC99kyQAAAA8AAAANc3RhYmxlX2lzc3VlcgAAAAAAABIAAAAAAAAAAEM5BlXva0R5UN6SCMY+6evwJa4mY/f062z0TKLnqN4wAAAAEAAAAAEAAAACAAAADwAAAAhDdXJyZW5jeQAAAA8AAAADZXVyAAAAABEAAAABAAAABQAAAA8AAAAGYWN0aXZlAAAAAAAAAAAAAQAAAA8AAAAIY29udHJhY3QAAAASAAAAAUGpebFxuPbvxZFzOxh8TWAxUwFgraPxPuJEY/8yhiYEAAAADwAAAAxkZW5vbWluYXRpb24AAAAPAAAAA2V1cgAAAAAPAAAAC2xhc3RfdXBkYXRlAAAAAAUAAAAAZVVB/AAAAA8AAAAEcmF0ZQAAAAkAAAAAAAAAAAAAAAAAEQs+AAAAEAAAAAEAAAACAAAADwAAAAhDdXJyZW5jeQAAAA8AAAADdXNkAAAAABEAAAABAAAABQAAAA8AAAAGYWN0aXZlAAAAAAAAAAAAAQAAAA8AAAAIY29udHJhY3QAAAASAAAAATUEqdkvrE2LnSiwOwed3v4VEaulOEiS1rxQw6rJkfxCAAAADwAAAAxkZW5vbWluYXRpb24AAAAPAAAAA3VzZAAAAAAPAAAAC2xhc3RfdXBkYXRlAAAAAAUAAAAAZVVB9wAAAA8AAAAEcmF0ZQAAAAkAAAAAAAAAAAAAAAAAEnzuAAAAEAAAAAEAAAACAAAADwAAAApWYXVsdHNJbmZvAAAAAAAPAAAAA2V1cgAAAAARAAAAAQAAAAgAAAAPAAAADGRlbm9taW5hdGlvbgAAAA8AAAADZXVyAAAAAA8AAAAKbG93ZXN0X2tleQAAAAAAEAAAAAEAAAACAAAADwAAAARTb21lAAAAEQAAAAEAAAADAAAADwAAAAdhY2NvdW50AAAAABIAAAAAAAAAAGKaH7iFUU2kfGOJGONeYuJ2U2QUeQ+zOEfYZvAoeHDsAAAADwAAAAxkZW5vbWluYXRpb24AAAAPAAAAA2V1cgAAAAAPAAAABWluZGV4AAAAAAAACQAAAAAAAAAAAAAAA7msoAAAAAAPAAAADG1pbl9jb2xfcmF0ZQAAAAkAAAAAAAAAAAAAAAAAp9jAAAAADwAAABFtaW5fZGVidF9jcmVhdGlvbgAAAAAAAAkAAAAAAAAAAAAAAAA7msoAAAAADwAAABBvcGVuaW5nX2NvbF9yYXRlAAAACQAAAAAAAAAAAAAAAACveeAAAAAPAAAACXRvdGFsX2NvbAAAAAAAAAkAAAAAAAAAAAAAAAlQL5AAAAAADwAAAAp0b3RhbF9kZWJ0AAAAAAAJAAAAAAAAAAAAAAAAlQL5AAAAAA8AAAAMdG90YWxfdmF1bHRzAAAABQAAAAAAAAABAAAAEAAAAAEAAAACAAAADwAAAApWYXVsdHNJbmZvAAAAAAAPAAAAA3VzZAAAAAARAAAAAQAAAAgAAAAPAAAADGRlbm9taW5hdGlvbgAAAA8AAAADdXNkAAAAAA8AAAAKbG93ZXN0X2tleQAAAAAAEAAAAAEAAAACAAAADwAAAARTb21lAAAAEQAAAAEAAAADAAAADwAAAAdhY2NvdW50AAAAABIAAAAAAAAAAGKaH7iFUU2kfGOJGONeYuJ2U2QUeQ+zOEfYZvAoeHDsAAAADwAAAAxkZW5vbWluYXRpb24AAAAPAAAAA3VzZAAAAAAPAAAABWluZGV4AAAAAAAACQAAAAAAAAAAAAAAA7msoAAAAAAPAAAADG1pbl9jb2xfcmF0ZQAAAAkAAAAAAAAAAAAAAAAAp9jAAAAADwAAABFtaW5fZGVidF9jcmVhdGlvbgAAAAAAAAkAAAAAAAAAAAAAAAA7msoAAAAADwAAABBvcGVuaW5nX2NvbF9yYXRlAAAACQAAAAAAAAAAAAAAAACveeAAAAAPAAAACXRvdGFsX2NvbAAAAAAAAAkAAAAAAAAAAAAAABF2WS4AAAAADwAAAAp0b3RhbF9kZWJ0AAAAAAAJAAAAAAAAAAAAAAAA7msoAAAAAA8AAAAMdG90YWxfdmF1bHRzAAAABQAAAAAAAAACAAAAAAAAAAAAAAABAAAAAAAAAAAAAAABAAAAFQAAAAEAAAAAAAAAAAAAAAIAAAAAAAAAAwAAAA8AAAAHZm5fY2FsbAAAAAANAAAAIIYTsCPkS9fGaZO3KiOaUUX9C/eoxPIvtMd3pIbgYdnFAAAADwAAABFzZXRfY3VycmVuY3lfcmF0ZQAAAAAAABAAAAABAAAAAgAAAA8AAAADZXVyAAAAAAkAAAAAAAAAAAAAAAAAEQs+AAAAAQAAAAAAAAABhhOwI+RL18Zpk7cqI5pRRf0L96jE8i+0x3ekhuBh2cUAAAACAAAAAAAAAAIAAAAPAAAACWZuX3JldHVybgAAAAAAAA8AAAARc2V0X2N1cnJlbmN5X3JhdGUAAAAAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAADwAAAAxjb3JlX21ldHJpY3MAAAAPAAAACnJlYWRfZW50cnkAAAAAAAUAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAA8AAAAMY29yZV9tZXRyaWNzAAAADwAAAAt3cml0ZV9lbnRyeQAAAAAFAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAPAAAADGNvcmVfbWV0cmljcwAAAA8AAAAQbGVkZ2VyX3JlYWRfYnl0ZQAAAAUAAAAAAACJaAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAA8AAAAMY29yZV9tZXRyaWNzAAAADwAAABFsZWRnZXJfd3JpdGVfYnl0ZQAAAAAAAAUAAAAAAAAHxAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAA8AAAAMY29yZV9tZXRyaWNzAAAADwAAAA1yZWFkX2tleV9ieXRlAAAAAAAABQAAAAAAAABUAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAADwAAAAxjb3JlX21ldHJpY3MAAAAPAAAADndyaXRlX2tleV9ieXRlAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAPAAAADGNvcmVfbWV0cmljcwAAAA8AAAAOcmVhZF9kYXRhX2J5dGUAAAAAAAUAAAAAAAAH6AAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAA8AAAAMY29yZV9tZXRyaWNzAAAADwAAAA93cml0ZV9kYXRhX2J5dGUAAAAABQAAAAAAAAfEAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAADwAAAAxjb3JlX21ldHJpY3MAAAAPAAAADnJlYWRfY29kZV9ieXRlAAAAAAAFAAAAAAAAgYAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAPAAAADGNvcmVfbWV0cmljcwAAAA8AAAAPd3JpdGVfY29kZV9ieXRlAAAAAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAA8AAAAMY29yZV9tZXRyaWNzAAAADwAAAAplbWl0X2V2ZW50AAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAPAAAADGNvcmVfbWV0cmljcwAAAA8AAAAPZW1pdF9ldmVudF9ieXRlAAAAAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAA8AAAAMY29yZV9tZXRyaWNzAAAADwAAAAhjcHVfaW5zbgAAAAUAAAAAATLTQAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAA8AAAAMY29yZV9tZXRyaWNzAAAADwAAAAhtZW1fYnl0ZQAAAAUAAAAAACqhewAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAA8AAAAMY29yZV9tZXRyaWNzAAAADwAAABFpbnZva2VfdGltZV9uc2VjcwAAAAAAAAUAAAAAABFfSQAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAA8AAAAMY29yZV9tZXRyaWNzAAAADwAAAA9tYXhfcndfa2V5X2J5dGUAAAAABQAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAADwAAAAxjb3JlX21ldHJpY3MAAAAPAAAAEG1heF9yd19kYXRhX2J5dGUAAAAFAAAAAAAAB+gAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAPAAAADGNvcmVfbWV0cmljcwAAAA8AAAAQbWF4X3J3X2NvZGVfYnl0ZQAAAAUAAAAAAACBgAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAA8AAAAMY29yZV9tZXRyaWNzAAAADwAAABNtYXhfZW1pdF9ldmVudF9ieXRlAAAAAAUAAAAAAAAAAA==", + "ledger": 2540064, + "createdAt": "1700086268" + } + }`)), + } + + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(&httpResponse, nil). + Once() + + result, err := rpcService.GetTransaction(transactionHash) + require.NoError(t, err) + + assert.Equal(t, entities.RPCGetTransactionResult{ + Status: "SUCCESS", + LatestLedger: 2540076, + LatestLedgerCloseTime: "1700086333", + OldestLedger: 2538637, + OldestLedgerCloseTime: "1700078796", + ApplicationOrder: 1, + EnvelopeXDR: "AAAAAgAAAADGFY14/R1KD0VGtTbi5Yp4d7LuMW0iQbLM/AUiGKj5owCpsoQAJY3OAAAjqgAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAGAAAAAAAAAABhhOwI+RL18Zpk7cqI5pRRf0L96jE8i+0x3ekhuBh2cUAAAARc2V0X2N1cnJlbmN5X3JhdGUAAAAAAAACAAAADwAAAANldXIAAAAACQAAAAAAAAAAAAAAAAARCz4AAAABAAAAAAAAAAAAAAABhhOwI+RL18Zpk7cqI5pRRf0L96jE8i+0x3ekhuBh2cUAAAARc2V0X2N1cnJlbmN5X3JhdGUAAAAAAAACAAAADwAAAANldXIAAAAACQAAAAAAAAAAAAAAAAARCz4AAAAAAAAAAQAAAAAAAAABAAAAB4408vVXuLU3mry897TfPpYjjsSN7n42REos241RddYdAAAAAQAAAAYAAAABhhOwI+RL18Zpk7cqI5pRRf0L96jE8i+0x3ekhuBh2cUAAAAUAAAAAQFvcYAAAImAAAAHxAAAAAAAAAACAAAAARio+aMAAABATbFMyom/TUz87wHex0LoYZA8jbNJkXbaDSgmOdk+wSBFJuMuta+/vSlro0e0vK2+1FqD/zWHZeYig4pKmM3rDA==", + ResultXDR: "AAAAAAARFy8AAAAAAAAAAQAAAAAAAAAYAAAAAMu8SHUN67hTUJOz3q+IrH9M/4dCVXaljeK6x1Ss20YWAAAAAA==", + ResultMetaXDR: "AAAAAwAAAAAAAAACAAAAAwAmwiAAAAAAAAAAAMYVjXj9HUoPRUa1NuLlinh3su4xbSJBssz8BSIYqPmjAAAAFUHZob0AJY3OAAAjqQAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAMAAAAAACbCHwAAAABlVUH3AAAAAAAAAAEAJsIgAAAAAAAAAADGFY14/R1KD0VGtTbi5Yp4d7LuMW0iQbLM/AUiGKj5owAAABVB2aG9ACWNzgAAI6oAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAADAAAAAAAmwiAAAAAAZVVB/AAAAAAAAAABAAAAAgAAAAMAJsIfAAAABgAAAAAAAAABhhOwI+RL18Zpk7cqI5pRRf0L96jE8i+0x3ekhuBh2cUAAAAUAAAAAQAAABMAAAAAjjTy9Ve4tTeavLz3tN8+liOOxI3ufjZESizbjVF11h0AAAABAAAABQAAABAAAAABAAAAAQAAAA8AAAAJQ29yZVN0YXRlAAAAAAAAEQAAAAEAAAAGAAAADwAAAAVhZG1pbgAAAAAAABIAAAAAAAAAADn1LT+CCK/HiHMChoEi/AtPrkos4XRR2E45Pr25lb3/AAAADwAAAAljb2xfdG9rZW4AAAAAAAASAAAAAdeSi3LCcDzP6vfrn/TvTVBKVai5efybRQ6iyEK00c5hAAAADwAAAAxvcmFjbGVfYWRtaW4AAAASAAAAAAAAAADGFY14/R1KD0VGtTbi5Yp4d7LuMW0iQbLM/AUiGKj5owAAAA8AAAAKcGFuaWNfbW9kZQAAAAAAAAAAAAAAAAAPAAAAEHByb3RvY29sX21hbmFnZXIAAAASAAAAAAAAAAAtSfyAwmj05lZ0WduHsQYQZgvahCNVtZyqS2HRC99kyQAAAA8AAAANc3RhYmxlX2lzc3VlcgAAAAAAABIAAAAAAAAAAEM5BlXva0R5UN6SCMY+6evwJa4mY/f062z0TKLnqN4wAAAAEAAAAAEAAAACAAAADwAAAAhDdXJyZW5jeQAAAA8AAAADZXVyAAAAABEAAAABAAAABQAAAA8AAAAGYWN0aXZlAAAAAAAAAAAAAQAAAA8AAAAIY29udHJhY3QAAAASAAAAAUGpebFxuPbvxZFzOxh8TWAxUwFgraPxPuJEY/8yhiYEAAAADwAAAAxkZW5vbWluYXRpb24AAAAPAAAAA2V1cgAAAAAPAAAAC2xhc3RfdXBkYXRlAAAAAAUAAAAAZVVBvgAAAA8AAAAEcmF0ZQAAAAkAAAAAAAAAAAAAAAAAEQb8AAAAEAAAAAEAAAACAAAADwAAAAhDdXJyZW5jeQAAAA8AAAADdXNkAAAAABEAAAABAAAABQAAAA8AAAAGYWN0aXZlAAAAAAAAAAAAAQAAAA8AAAAIY29udHJhY3QAAAASAAAAATUEqdkvrE2LnSiwOwed3v4VEaulOEiS1rxQw6rJkfxCAAAADwAAAAxkZW5vbWluYXRpb24AAAAPAAAAA3VzZAAAAAAPAAAAC2xhc3RfdXBkYXRlAAAAAAUAAAAAZVVB9wAAAA8AAAAEcmF0ZQAAAAkAAAAAAAAAAAAAAAAAEnzuAAAAEAAAAAEAAAACAAAADwAAAApWYXVsdHNJbmZvAAAAAAAPAAAAA2V1cgAAAAARAAAAAQAAAAgAAAAPAAAADGRlbm9taW5hdGlvbgAAAA8AAAADZXVyAAAAAA8AAAAKbG93ZXN0X2tleQAAAAAAEAAAAAEAAAACAAAADwAAAARTb21lAAAAEQAAAAEAAAADAAAADwAAAAdhY2NvdW50AAAAABIAAAAAAAAAAGKaH7iFUU2kfGOJGONeYuJ2U2QUeQ+zOEfYZvAoeHDsAAAADwAAAAxkZW5vbWluYXRpb24AAAAPAAAAA2V1cgAAAAAPAAAABWluZGV4AAAAAAAACQAAAAAAAAAAAAAAA7msoAAAAAAPAAAADG1pbl9jb2xfcmF0ZQAAAAkAAAAAAAAAAAAAAAAAp9jAAAAADwAAABFtaW5fZGVidF9jcmVhdGlvbgAAAAAAAAkAAAAAAAAAAAAAAAA7msoAAAAADwAAABBvcGVuaW5nX2NvbF9yYXRlAAAACQAAAAAAAAAAAAAAAACveeAAAAAPAAAACXRvdGFsX2NvbAAAAAAAAAkAAAAAAAAAAAAAAAlQL5AAAAAADwAAAAp0b3RhbF9kZWJ0AAAAAAAJAAAAAAAAAAAAAAAAlQL5AAAAAA8AAAAMdG90YWxfdmF1bHRzAAAABQAAAAAAAAABAAAAEAAAAAEAAAACAAAADwAAAApWYXVsdHNJbmZvAAAAAAAPAAAAA3VzZAAAAAARAAAAAQAAAAgAAAAPAAAADGRlbm9taW5hdGlvbgAAAA8AAAADdXNkAAAAAA8AAAAKbG93ZXN0X2tleQAAAAAAEAAAAAEAAAACAAAADwAAAARTb21lAAAAEQAAAAEAAAADAAAADwAAAAdhY2NvdW50AAAAABIAAAAAAAAAAGKaH7iFUU2kfGOJGONeYuJ2U2QUeQ+zOEfYZvAoeHDsAAAADwAAAAxkZW5vbWluYXRpb24AAAAPAAAAA3VzZAAAAAAPAAAABWluZGV4AAAAAAAACQAAAAAAAAAAAAAAA7msoAAAAAAPAAAADG1pbl9jb2xfcmF0ZQAAAAkAAAAAAAAAAAAAAAAAp9jAAAAADwAAABFtaW5fZGVidF9jcmVhdGlvbgAAAAAAAAkAAAAAAAAAAAAAAAA7msoAAAAADwAAABBvcGVuaW5nX2NvbF9yYXRlAAAACQAAAAAAAAAAAAAAAACveeAAAAAPAAAACXRvdGFsX2NvbAAAAAAAAAkAAAAAAAAAAAAAABF2WS4AAAAADwAAAAp0b3RhbF9kZWJ0AAAAAAAJAAAAAAAAAAAAAAAA7msoAAAAAA8AAAAMdG90YWxfdmF1bHRzAAAABQAAAAAAAAACAAAAAAAAAAEAJsIgAAAABgAAAAAAAAABhhOwI+RL18Zpk7cqI5pRRf0L96jE8i+0x3ekhuBh2cUAAAAUAAAAAQAAABMAAAAAjjTy9Ve4tTeavLz3tN8+liOOxI3ufjZESizbjVF11h0AAAABAAAABQAAABAAAAABAAAAAQAAAA8AAAAJQ29yZVN0YXRlAAAAAAAAEQAAAAEAAAAGAAAADwAAAAVhZG1pbgAAAAAAABIAAAAAAAAAADn1LT+CCK/HiHMChoEi/AtPrkos4XRR2E45Pr25lb3/AAAADwAAAAljb2xfdG9rZW4AAAAAAAASAAAAAdeSi3LCcDzP6vfrn/TvTVBKVai5efybRQ6iyEK00c5hAAAADwAAAAxvcmFjbGVfYWRtaW4AAAASAAAAAAAAAADGFY14/R1KD0VGtTbi5Yp4d7LuMW0iQbLM/AUiGKj5owAAAA8AAAAKcGFuaWNfbW9kZQAAAAAAAAAAAAAAAAAPAAAAEHByb3RvY29sX21hbmFnZXIAAAASAAAAAAAAAAAtSfyAwmj05lZ0WduHsQYQZgvahCNVtZyqS2HRC99kyQAAAA8AAAANc3RhYmxlX2lzc3VlcgAAAAAAABIAAAAAAAAAAEM5BlXva0R5UN6SCMY+6evwJa4mY/f062z0TKLnqN4wAAAAEAAAAAEAAAACAAAADwAAAAhDdXJyZW5jeQAAAA8AAAADZXVyAAAAABEAAAABAAAABQAAAA8AAAAGYWN0aXZlAAAAAAAAAAAAAQAAAA8AAAAIY29udHJhY3QAAAASAAAAAUGpebFxuPbvxZFzOxh8TWAxUwFgraPxPuJEY/8yhiYEAAAADwAAAAxkZW5vbWluYXRpb24AAAAPAAAAA2V1cgAAAAAPAAAAC2xhc3RfdXBkYXRlAAAAAAUAAAAAZVVB/AAAAA8AAAAEcmF0ZQAAAAkAAAAAAAAAAAAAAAAAEQs+AAAAEAAAAAEAAAACAAAADwAAAAhDdXJyZW5jeQAAAA8AAAADdXNkAAAAABEAAAABAAAABQAAAA8AAAAGYWN0aXZlAAAAAAAAAAAAAQAAAA8AAAAIY29udHJhY3QAAAASAAAAATUEqdkvrE2LnSiwOwed3v4VEaulOEiS1rxQw6rJkfxCAAAADwAAAAxkZW5vbWluYXRpb24AAAAPAAAAA3VzZAAAAAAPAAAAC2xhc3RfdXBkYXRlAAAAAAUAAAAAZVVB9wAAAA8AAAAEcmF0ZQAAAAkAAAAAAAAAAAAAAAAAEnzuAAAAEAAAAAEAAAACAAAADwAAAApWYXVsdHNJbmZvAAAAAAAPAAAAA2V1cgAAAAARAAAAAQAAAAgAAAAPAAAADGRlbm9taW5hdGlvbgAAAA8AAAADZXVyAAAAAA8AAAAKbG93ZXN0X2tleQAAAAAAEAAAAAEAAAACAAAADwAAAARTb21lAAAAEQAAAAEAAAADAAAADwAAAAdhY2NvdW50AAAAABIAAAAAAAAAAGKaH7iFUU2kfGOJGONeYuJ2U2QUeQ+zOEfYZvAoeHDsAAAADwAAAAxkZW5vbWluYXRpb24AAAAPAAAAA2V1cgAAAAAPAAAABWluZGV4AAAAAAAACQAAAAAAAAAAAAAAA7msoAAAAAAPAAAADG1pbl9jb2xfcmF0ZQAAAAkAAAAAAAAAAAAAAAAAp9jAAAAADwAAABFtaW5fZGVidF9jcmVhdGlvbgAAAAAAAAkAAAAAAAAAAAAAAAA7msoAAAAADwAAABBvcGVuaW5nX2NvbF9yYXRlAAAACQAAAAAAAAAAAAAAAACveeAAAAAPAAAACXRvdGFsX2NvbAAAAAAAAAkAAAAAAAAAAAAAAAlQL5AAAAAADwAAAAp0b3RhbF9kZWJ0AAAAAAAJAAAAAAAAAAAAAAAAlQL5AAAAAA8AAAAMdG90YWxfdmF1bHRzAAAABQAAAAAAAAABAAAAEAAAAAEAAAACAAAADwAAAApWYXVsdHNJbmZvAAAAAAAPAAAAA3VzZAAAAAARAAAAAQAAAAgAAAAPAAAADGRlbm9taW5hdGlvbgAAAA8AAAADdXNkAAAAAA8AAAAKbG93ZXN0X2tleQAAAAAAEAAAAAEAAAACAAAADwAAAARTb21lAAAAEQAAAAEAAAADAAAADwAAAAdhY2NvdW50AAAAABIAAAAAAAAAAGKaH7iFUU2kfGOJGONeYuJ2U2QUeQ+zOEfYZvAoeHDsAAAADwAAAAxkZW5vbWluYXRpb24AAAAPAAAAA3VzZAAAAAAPAAAABWluZGV4AAAAAAAACQAAAAAAAAAAAAAAA7msoAAAAAAPAAAADG1pbl9jb2xfcmF0ZQAAAAkAAAAAAAAAAAAAAAAAp9jAAAAADwAAABFtaW5fZGVidF9jcmVhdGlvbgAAAAAAAAkAAAAAAAAAAAAAAAA7msoAAAAADwAAABBvcGVuaW5nX2NvbF9yYXRlAAAACQAAAAAAAAAAAAAAAACveeAAAAAPAAAACXRvdGFsX2NvbAAAAAAAAAkAAAAAAAAAAAAAABF2WS4AAAAADwAAAAp0b3RhbF9kZWJ0AAAAAAAJAAAAAAAAAAAAAAAA7msoAAAAAA8AAAAMdG90YWxfdmF1bHRzAAAABQAAAAAAAAACAAAAAAAAAAAAAAABAAAAAAAAAAAAAAABAAAAFQAAAAEAAAAAAAAAAAAAAAIAAAAAAAAAAwAAAA8AAAAHZm5fY2FsbAAAAAANAAAAIIYTsCPkS9fGaZO3KiOaUUX9C/eoxPIvtMd3pIbgYdnFAAAADwAAABFzZXRfY3VycmVuY3lfcmF0ZQAAAAAAABAAAAABAAAAAgAAAA8AAAADZXVyAAAAAAkAAAAAAAAAAAAAAAAAEQs+AAAAAQAAAAAAAAABhhOwI+RL18Zpk7cqI5pRRf0L96jE8i+0x3ekhuBh2cUAAAACAAAAAAAAAAIAAAAPAAAACWZuX3JldHVybgAAAAAAAA8AAAARc2V0X2N1cnJlbmN5X3JhdGUAAAAAAAABAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAADwAAAAxjb3JlX21ldHJpY3MAAAAPAAAACnJlYWRfZW50cnkAAAAAAAUAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAA8AAAAMY29yZV9tZXRyaWNzAAAADwAAAAt3cml0ZV9lbnRyeQAAAAAFAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAPAAAADGNvcmVfbWV0cmljcwAAAA8AAAAQbGVkZ2VyX3JlYWRfYnl0ZQAAAAUAAAAAAACJaAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAA8AAAAMY29yZV9tZXRyaWNzAAAADwAAABFsZWRnZXJfd3JpdGVfYnl0ZQAAAAAAAAUAAAAAAAAHxAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAA8AAAAMY29yZV9tZXRyaWNzAAAADwAAAA1yZWFkX2tleV9ieXRlAAAAAAAABQAAAAAAAABUAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAADwAAAAxjb3JlX21ldHJpY3MAAAAPAAAADndyaXRlX2tleV9ieXRlAAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAPAAAADGNvcmVfbWV0cmljcwAAAA8AAAAOcmVhZF9kYXRhX2J5dGUAAAAAAAUAAAAAAAAH6AAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAA8AAAAMY29yZV9tZXRyaWNzAAAADwAAAA93cml0ZV9kYXRhX2J5dGUAAAAABQAAAAAAAAfEAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAADwAAAAxjb3JlX21ldHJpY3MAAAAPAAAADnJlYWRfY29kZV9ieXRlAAAAAAAFAAAAAAAAgYAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAPAAAADGNvcmVfbWV0cmljcwAAAA8AAAAPd3JpdGVfY29kZV9ieXRlAAAAAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAA8AAAAMY29yZV9tZXRyaWNzAAAADwAAAAplbWl0X2V2ZW50AAAAAAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAPAAAADGNvcmVfbWV0cmljcwAAAA8AAAAPZW1pdF9ldmVudF9ieXRlAAAAAAUAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAA8AAAAMY29yZV9tZXRyaWNzAAAADwAAAAhjcHVfaW5zbgAAAAUAAAAAATLTQAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAA8AAAAMY29yZV9tZXRyaWNzAAAADwAAAAhtZW1fYnl0ZQAAAAUAAAAAACqhewAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAA8AAAAMY29yZV9tZXRyaWNzAAAADwAAABFpbnZva2VfdGltZV9uc2VjcwAAAAAAAAUAAAAAABFfSQAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAA8AAAAMY29yZV9tZXRyaWNzAAAADwAAAA9tYXhfcndfa2V5X2J5dGUAAAAABQAAAAAAAAAwAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAACAAAADwAAAAxjb3JlX21ldHJpY3MAAAAPAAAAEG1heF9yd19kYXRhX2J5dGUAAAAFAAAAAAAAB+gAAAAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAPAAAADGNvcmVfbWV0cmljcwAAAA8AAAAQbWF4X3J3X2NvZGVfYnl0ZQAAAAUAAAAAAACBgAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAA8AAAAMY29yZV9tZXRyaWNzAAAADwAAABNtYXhfZW1pdF9ldmVudF9ieXRlAAAAAAUAAAAAAAAAAA==", + Ledger: 2540064, + CreatedAt: "1700086268", + ErrorResultXDR: "", + }, result) + }) + + t.Run("rpc_request_fails", func(t *testing.T) { + mockHTTPClient. + On("Post", rpcURL, "application/json", mock.Anything). + Return(&http.Response{}, errors.New("connection failed")). + Once() + + result, err := rpcService.GetTransaction("hash") + require.Error(t, err) + + assert.Equal(t, entities.RPCGetTransactionResult{}, result) + assert.Equal(t, "sending getTransaction request: sending POST request to RPC: connection failed", err.Error()) + }) +} diff --git a/internal/services/servicesmocks/rpc_service_mocks.go b/internal/services/servicesmocks/rpc_service_mocks.go new file mode 100644 index 0000000..f72c348 --- /dev/null +++ b/internal/services/servicesmocks/rpc_service_mocks.go @@ -0,0 +1,27 @@ +package servicesmocks + +import ( + "github.com/stellar/wallet-backend/internal/entities" + "github.com/stellar/wallet-backend/internal/services" + "github.com/stretchr/testify/mock" +) + +type RPCServiceMock struct { + mock.Mock +} + +var _ services.RPCService = (*RPCServiceMock)(nil) + +func (r *RPCServiceMock) SendTransaction(transactionXdr string) (entities.RPCSendTransactionResult, error) { + args := r.Called(transactionXdr) + return args.Get(0).(entities.RPCSendTransactionResult), args.Error(1) +} + +func (r *RPCServiceMock) GetTransaction(transactionHash string) (entities.RPCGetTransactionResult, error) { + args := r.Called(transactionHash) + return args.Get(0).(entities.RPCGetTransactionResult), args.Error(1) +} + +type TransactionManagerMock struct { + mock.Mock +} diff --git a/internal/tss/channels/error_handler_service_non_jitter_channel_test.go b/internal/tss/channels/error_handler_service_non_jitter_channel_test.go deleted file mode 100644 index 51eae3d..0000000 --- a/internal/tss/channels/error_handler_service_non_jitter_channel_test.go +++ /dev/null @@ -1,224 +0,0 @@ -package channels - -import ( - "context" - "errors" - "testing" - - "github.com/stellar/go/xdr" - "github.com/stellar/wallet-backend/internal/db" - "github.com/stellar/wallet-backend/internal/db/dbtest" - "github.com/stellar/wallet-backend/internal/tss" - "github.com/stellar/wallet-backend/internal/tss/router" - "github.com/stellar/wallet-backend/internal/tss/store" - "github.com/stellar/wallet-backend/internal/tss/utils" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" -) - -func TestNonJitterSend(t *testing.T) { - dbt := dbtest.Open(t) - defer dbt.Close() - - dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) - require.NoError(t, err) - defer dbConnectionPool.Close() - store := store.NewStore(dbConnectionPool) - txServiceMock := utils.TransactionServiceMock{} - cfg := RPCErrorHandlerServiceNonJitterChannelConfigs{ - Store: store, - TxService: &txServiceMock, - MaxBufferSize: 1, - MaxWorkers: 1, - MaxRetries: 3, - WaitBtwnRetriesMS: 10, - } - channel := NewErrorHandlerServiceNonJitterChannel(cfg) - - payload := tss.Payload{} - payload.WebhookURL = "www.stellar.com" - payload.TransactionHash = "hash" - payload.TransactionXDR = "xdr" - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(nil, errors.New("signing failed")) - - _ = store.UpsertTransaction(context.Background(), payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.NewStatus) - - channel.Send(payload) - channel.Stop() - - var status string - err = dbConnectionPool.GetContext(context.Background(), &status, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, status, string(tss.NewStatus)) -} - -func TestNonJitterReceive(t *testing.T) { - dbt := dbtest.Open(t) - defer dbt.Close() - - dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) - require.NoError(t, err) - defer dbConnectionPool.Close() - store := store.NewStore(dbConnectionPool) - txServiceMock := utils.TransactionServiceMock{} - cfg := RPCErrorHandlerServiceNonJitterChannelConfigs{ - Store: store, - TxService: &txServiceMock, - MaxBufferSize: 1, - MaxWorkers: 1, - MaxRetries: 3, - WaitBtwnRetriesMS: 10, - } - channel := NewErrorHandlerServiceNonJitterChannel(cfg) - - mockRouter := router.MockRouter{} - defer mockRouter.AssertExpectations(t) - channel.SetRouter(&mockRouter) - networkPass := "passphrase" - feeBumpTx := utils.BuildTestFeeBumpTransaction() - feeBumpTxXDR, _ := feeBumpTx.Base64() - feeBumpTxHash, _ := feeBumpTx.HashHex(networkPass) - payload := tss.Payload{} - payload.WebhookURL = "www.stellar.com" - payload.TransactionHash = "hash" - payload.TransactionXDR = "xdr" - - _ = store.UpsertTransaction(context.Background(), payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.NewStatus) - - t.Run("signing_and_submitting_tx_fails", func(t *testing.T) { - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(nil, errors.New("sign tx failed")). - Once() - - channel.Receive(payload) - - var txStatus string - err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, string(tss.NewStatus), txStatus) - - }) - t.Run("payload_gets_routed", func(t *testing.T) { - sendResp := tss.RPCSendTxResponse{} - sendResp.Status = tss.TryAgainLaterStatus - sendResp.TransactionHash = feeBumpTxHash - sendResp.TransactionXDR = feeBumpTxXDR - sendResp.Code.TxResultCode = xdr.TransactionResultCodeTxInsufficientFee - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(feeBumpTx, nil). - Once(). - On("NetworkPassphrase"). - Return(networkPass). - Once(). - On("SendTransaction", feeBumpTxXDR). - Return(sendResp, nil). - Once() - - mockRouter. - On("Route", mock.AnythingOfType("tss.Payload")). - Return(). - Once() - - channel.Receive(payload) - - var txStatus string - err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, string(tss.TryAgainLaterStatus), txStatus) - - var tryStatus int - err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) - require.NoError(t, err) - assert.Equal(t, int(xdr.TransactionResultCodeTxInsufficientFee), tryStatus) - }) - - t.Run("retries", func(t *testing.T) { - sendResp1 := tss.RPCSendTxResponse{} - sendResp1.Status = tss.ErrorStatus - sendResp1.TransactionHash = feeBumpTxHash - sendResp1.TransactionXDR = feeBumpTxXDR - sendResp1.Code.TxResultCode = xdr.TransactionResultCodeTxTooEarly - - sendResp2 := tss.RPCSendTxResponse{} - sendResp2.Status = tss.TryAgainLaterStatus - sendResp2.TransactionHash = feeBumpTxHash - sendResp2.TransactionXDR = feeBumpTxXDR - sendResp2.Code.TxResultCode = xdr.TransactionResultCodeTxInsufficientFee - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(feeBumpTx, nil). - Twice(). - On("NetworkPassphrase"). - Return(networkPass). - Twice() - - txServiceMock. - On("SendTransaction", feeBumpTxXDR). - Return(sendResp1, nil). - Once() - - txServiceMock. - On("SendTransaction", feeBumpTxXDR). - Return(sendResp2, nil). - Once() - - mockRouter. - On("Route", mock.AnythingOfType("tss.Payload")). - Return(). - Once() - - channel.Receive(payload) - - var txStatus string - err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, string(tss.TryAgainLaterStatus), txStatus) - - var tryStatus int - err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) - require.NoError(t, err) - assert.Equal(t, int(xdr.TransactionResultCodeTxInsufficientFee), tryStatus) - }) - - t.Run("max_retries", func(t *testing.T) { - sendResp := tss.RPCSendTxResponse{} - sendResp.Status = tss.ErrorStatus - sendResp.TransactionHash = feeBumpTxHash - sendResp.TransactionXDR = feeBumpTxXDR - sendResp.Code.TxResultCode = xdr.TransactionResultCodeTxTooEarly - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(feeBumpTx, nil). - Times(3). - On("NetworkPassphrase"). - Return(networkPass). - Times(3) - - txServiceMock. - On("SendTransaction", feeBumpTxXDR). - Return(sendResp, nil). - Times(3) - - mockRouter. - On("Route", mock.AnythingOfType("tss.Payload")). - Return(). - Once() - - channel.Receive(payload) - - var txStatus string - err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, string(tss.ErrorStatus), txStatus) - - var tryStatus int - err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) - require.NoError(t, err) - assert.Equal(t, int(xdr.TransactionResultCodeTxTooEarly), tryStatus) - }) -} diff --git a/internal/tss/channels/error_handler_service_jitter_channel.go b/internal/tss/channels/error_jitter_channel.go similarity index 51% rename from internal/tss/channels/error_handler_service_jitter_channel.go rename to internal/tss/channels/error_jitter_channel.go index 931372c..52097f2 100644 --- a/internal/tss/channels/error_handler_service_jitter_channel.go +++ b/internal/tss/channels/error_jitter_channel.go @@ -9,13 +9,12 @@ import ( "github.com/stellar/go/support/log" "github.com/stellar/wallet-backend/internal/tss" "github.com/stellar/wallet-backend/internal/tss/router" - tss_store "github.com/stellar/wallet-backend/internal/tss/store" - "github.com/stellar/wallet-backend/internal/tss/utils" + "github.com/stellar/wallet-backend/internal/tss/services" + "golang.org/x/exp/rand" ) -type RPCErrorHandlerServiceJitterChannelConfigs struct { - Store tss_store.Store - TxService utils.TransactionService +type ErrorJitterChannelConfigs struct { + TxManager services.TransactionManager Router router.Router MaxBufferSize int MaxWorkers int @@ -23,60 +22,74 @@ type RPCErrorHandlerServiceJitterChannelConfigs struct { MinWaitBtwnRetriesMS int } -type rpcErrorHandlerServiceJitterPool struct { +type errorJitterPool struct { Pool *pond.WorkerPool - TxService utils.TransactionService - Store tss_store.Store + TxManager services.TransactionManager Router router.Router MaxRetries int MinWaitBtwnRetriesMS int } -func NewErrorHandlerServiceJitterChannel(cfg RPCErrorHandlerServiceJitterChannelConfigs) *rpcErrorHandlerServiceJitterPool { +var ErrorJitterChannelName = "ErrorJitterChannel" + +func jitter(dur time.Duration) time.Duration { + halfDur := int64(dur / 2) + delta := rand.Int63n(halfDur) - halfDur/2 + return dur + time.Duration(delta) +} + +func NewErrorJitterChannel(cfg ErrorJitterChannelConfigs) *errorJitterPool { pool := pond.New(cfg.MaxBufferSize, cfg.MaxWorkers, pond.Strategy(pond.Balanced())) - return &rpcErrorHandlerServiceJitterPool{ + return &errorJitterPool{ Pool: pool, - TxService: cfg.TxService, - Store: cfg.Store, + TxManager: cfg.TxManager, + Router: cfg.Router, MaxRetries: cfg.MaxRetries, MinWaitBtwnRetriesMS: cfg.MinWaitBtwnRetriesMS, } } -func (p *rpcErrorHandlerServiceJitterPool) Send(payload tss.Payload) { +func (p *errorJitterPool) Send(payload tss.Payload) { p.Pool.Submit(func() { p.Receive(payload) }) } -func (p *rpcErrorHandlerServiceJitterPool) Receive(payload tss.Payload) { +func (p *errorJitterPool) Receive(payload tss.Payload) { ctx := context.Background() var i int for i = 0; i < p.MaxRetries; i++ { currentBackoff := p.MinWaitBtwnRetriesMS * (1 << i) time.Sleep(jitter(time.Duration(currentBackoff)) * time.Microsecond) - rpcSendResp, err := BuildAndSubmitTransaction(ctx, "ErrorHandlerServiceJitterChannel", payload, p.Store, p.TxService) + rpcSendResp, err := p.TxManager.BuildAndSubmitTransaction(ctx, ErrorJitterChannelName, payload) if err != nil { - log.Errorf(err.Error()) + log.Errorf("%s: Unable to sign and submit transaction: %e", ErrorJitterChannelName, err) return } payload.RpcSubmitTxResponse = rpcSendResp if !slices.Contains(tss.JitterErrorCodes, rpcSendResp.Code.TxResultCode) { - p.Router.Route(payload) + err = p.Router.Route(payload) + if err != nil { + log.Errorf("%s: Unable to route payload: %e", ErrorJitterChannelName, err) + return + } return } } if i == p.MaxRetries { // Retry limit reached, route the payload to the router so it can re-route it to this pool and keep re-trying // NOTE: Is this a good idea? Infinite tries per transaction ? - p.Router.Route(payload) + err := p.Router.Route(payload) + if err != nil { + log.Errorf("%s: Unable to route payload: %e", ErrorJitterChannelName, err) + } } } -func (p *rpcErrorHandlerServiceJitterPool) SetRouter(router router.Router) { +func (p *errorJitterPool) SetRouter(router router.Router) { p.Router = router } -func (p *rpcErrorHandlerServiceJitterPool) Stop() { +func (p *errorJitterPool) Stop() { p.Pool.StopAndWait() } diff --git a/internal/tss/channels/error_jitter_channel_test.go b/internal/tss/channels/error_jitter_channel_test.go new file mode 100644 index 0000000..8dd746d --- /dev/null +++ b/internal/tss/channels/error_jitter_channel_test.go @@ -0,0 +1,141 @@ +package channels + +import ( + "context" + "errors" + "testing" + + "github.com/stellar/wallet-backend/internal/db" + "github.com/stellar/wallet-backend/internal/db/dbtest" + "github.com/stellar/wallet-backend/internal/entities" + "github.com/stellar/wallet-backend/internal/tss" + "github.com/stellar/wallet-backend/internal/tss/router" + "github.com/stellar/wallet-backend/internal/tss/services" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestJitterSend(t *testing.T) { + dbt := dbtest.Open(t) + defer dbt.Close() + dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) + require.NoError(t, err) + defer dbConnectionPool.Close() + txManagerMock := services.TransactionManagerMock{} + routerMock := router.MockRouter{} + cfg := ErrorJitterChannelConfigs{ + TxManager: &txManagerMock, + Router: &routerMock, + MaxBufferSize: 1, + MaxWorkers: 1, + MaxRetries: 3, + MinWaitBtwnRetriesMS: 10, + } + + channel := NewErrorJitterChannel(cfg) + + payload := tss.Payload{} + payload.WebhookURL = "www.stellar.com" + payload.TransactionHash = "hash" + payload.TransactionXDR = "xdr" + + rpcResp := tss.RPCSendTxResponse{ + Status: tss.RPCTXStatus{RPCStatus: entities.ErrorStatus}, + Code: tss.RPCTXCode{TxResultCode: tss.NonJitterErrorCodes[0]}, + } + payload.RpcSubmitTxResponse = rpcResp + + txManagerMock. + On("BuildAndSubmitTransaction", context.Background(), ErrorJitterChannelName, payload). + Return(rpcResp, nil). + Once() + + routerMock. + On("Route", payload). + Return(nil). + Once() + + channel.Send(payload) + channel.Stop() + + routerMock.AssertCalled(t, "Route", payload) +} + +func TestJitterReceive(t *testing.T) { + dbt := dbtest.Open(t) + defer dbt.Close() + dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) + require.NoError(t, err) + defer dbConnectionPool.Close() + txManagerMock := services.TransactionManagerMock{} + routerMock := router.MockRouter{} + cfg := ErrorJitterChannelConfigs{ + TxManager: &txManagerMock, + Router: &routerMock, + MaxBufferSize: 1, + MaxWorkers: 1, + MaxRetries: 3, + MinWaitBtwnRetriesMS: 10, + } + + channel := NewErrorJitterChannel(cfg) + + payload := tss.Payload{} + payload.WebhookURL = "www.stellar.com" + payload.TransactionHash = "hash" + payload.TransactionXDR = "xdr" + + t.Run("build_and_submit_tx_fail", func(t *testing.T) { + txManagerMock. + On("BuildAndSubmitTransaction", context.Background(), ErrorJitterChannelName, payload). + Return(tss.RPCSendTxResponse{}, errors.New("build tx failed")). + Once() + + channel.Receive(payload) + + routerMock.AssertNotCalled(t, "Route", payload) + }) + t.Run("retries", func(t *testing.T) { + sendResp1 := tss.RPCSendTxResponse{ + Status: tss.RPCTXStatus{RPCStatus: entities.ErrorStatus}, + Code: tss.RPCTXCode{TxResultCode: tss.JitterErrorCodes[0]}, + } + sendResp2 := tss.RPCSendTxResponse{ + Status: tss.RPCTXStatus{RPCStatus: entities.ErrorStatus}, + Code: tss.RPCTXCode{TxResultCode: tss.NonJitterErrorCodes[0]}, + } + + txManagerMock. + On("BuildAndSubmitTransaction", context.Background(), ErrorJitterChannelName, mock.AnythingOfType("tss.Payload")). + Return(sendResp1, nil). + Once() + txManagerMock. + On("BuildAndSubmitTransaction", context.Background(), ErrorJitterChannelName, mock.AnythingOfType("tss.Payload")). + Return(sendResp2, nil). + Once() + routerMock. + On("Route", mock.AnythingOfType("tss.Payload")). + Return(nil). + Once() + + channel.Receive(payload) + }) + + t.Run("max_retries", func(t *testing.T) { + sendResp := tss.RPCSendTxResponse{ + Status: tss.RPCTXStatus{RPCStatus: entities.ErrorStatus}, + Code: tss.RPCTXCode{TxResultCode: tss.JitterErrorCodes[0]}, + } + + txManagerMock. + On("BuildAndSubmitTransaction", context.Background(), ErrorJitterChannelName, mock.AnythingOfType("tss.Payload")). + Return(sendResp, nil). + Times(3) + routerMock. + On("Route", mock.AnythingOfType("tss.Payload")). + Return(nil). + Once() + + channel.Receive(payload) + }) +} diff --git a/internal/tss/channels/error_handler_service_non_jitter_channel.go b/internal/tss/channels/error_non_jitter_channel.go similarity index 53% rename from internal/tss/channels/error_handler_service_non_jitter_channel.go rename to internal/tss/channels/error_non_jitter_channel.go index 16508b0..af8a4f9 100644 --- a/internal/tss/channels/error_handler_service_non_jitter_channel.go +++ b/internal/tss/channels/error_non_jitter_channel.go @@ -2,6 +2,7 @@ package channels import ( "context" + "fmt" "slices" "time" @@ -9,13 +10,12 @@ import ( "github.com/stellar/go/support/log" "github.com/stellar/wallet-backend/internal/tss" "github.com/stellar/wallet-backend/internal/tss/router" + "github.com/stellar/wallet-backend/internal/tss/services" tss_store "github.com/stellar/wallet-backend/internal/tss/store" - "github.com/stellar/wallet-backend/internal/tss/utils" ) -type RPCErrorHandlerServiceNonJitterChannelConfigs struct { - Store tss_store.Store - TxService utils.TransactionService +type ErrorNonJitterChannelConfigs struct { + TxManager services.TransactionManager Router router.Router MaxBufferSize int MaxWorkers int @@ -23,59 +23,70 @@ type RPCErrorHandlerServiceNonJitterChannelConfigs struct { WaitBtwnRetriesMS int } -type rpcErrorHandlerServiceNonJitterPool struct { +type errorNonJitterPool struct { Pool *pond.WorkerPool - TxService utils.TransactionService + TxManager services.TransactionManager Store tss_store.Store Router router.Router MaxRetries int WaitBtwnRetriesMS int } -func NewErrorHandlerServiceNonJitterChannel(cfg RPCErrorHandlerServiceNonJitterChannelConfigs) *rpcErrorHandlerServiceNonJitterPool { +var ErrorNonJitterChannelName = "ErrorNonJitterChannel" + +func NewErrorNonJitterChannel(cfg ErrorNonJitterChannelConfigs) *errorNonJitterPool { pool := pond.New(cfg.MaxBufferSize, cfg.MaxWorkers, pond.Strategy(pond.Balanced())) - return &rpcErrorHandlerServiceNonJitterPool{ + return &errorNonJitterPool{ Pool: pool, - TxService: cfg.TxService, - Store: cfg.Store, + TxManager: cfg.TxManager, + Router: cfg.Router, MaxRetries: cfg.MaxRetries, WaitBtwnRetriesMS: cfg.WaitBtwnRetriesMS, } } -func (p *rpcErrorHandlerServiceNonJitterPool) Send(payload tss.Payload) { +func (p *errorNonJitterPool) Send(payload tss.Payload) { p.Pool.Submit(func() { p.Receive(payload) }) } -func (p *rpcErrorHandlerServiceNonJitterPool) Receive(payload tss.Payload) { +func (p *errorNonJitterPool) Receive(payload tss.Payload) { ctx := context.Background() var i int for i = 0; i < p.MaxRetries; i++ { + fmt.Println(i) time.Sleep(time.Duration(p.WaitBtwnRetriesMS) * time.Microsecond) - rpcSendResp, err := BuildAndSubmitTransaction(ctx, "ErrorHandlerServiceNonJitterChannel", payload, p.Store, p.TxService) + rpcSendResp, err := p.TxManager.BuildAndSubmitTransaction(ctx, ErrorNonJitterChannelName, payload) if err != nil { - log.Errorf(err.Error()) + log.Errorf("%s: Unable to sign and submit transaction: %e", ErrorNonJitterChannelName, err) return } payload.RpcSubmitTxResponse = rpcSendResp if !slices.Contains(tss.NonJitterErrorCodes, rpcSendResp.Code.TxResultCode) { - p.Router.Route(payload) + err := p.Router.Route(payload) + if err != nil { + log.Errorf("%s: Unable to route payload: %e", ErrorNonJitterChannelName, err) + return + } return } } if i == p.MaxRetries { // Retry limit reached, route the payload to the router so it can re-route it to this pool and keep re-trying // NOTE: Is this a good idea? - p.Router.Route(payload) + err := p.Router.Route(payload) + if err != nil { + log.Errorf("%s: Unable to route payload: %e", ErrorNonJitterChannelName, err) + return + } } } -func (p *rpcErrorHandlerServiceNonJitterPool) SetRouter(router router.Router) { +func (p *errorNonJitterPool) SetRouter(router router.Router) { p.Router = router } -func (p *rpcErrorHandlerServiceNonJitterPool) Stop() { +func (p *errorNonJitterPool) Stop() { p.Pool.StopAndWait() } diff --git a/internal/tss/channels/error_non_jitter_channel_test.go b/internal/tss/channels/error_non_jitter_channel_test.go new file mode 100644 index 0000000..94fef17 --- /dev/null +++ b/internal/tss/channels/error_non_jitter_channel_test.go @@ -0,0 +1,140 @@ +package channels + +import ( + "context" + "errors" + "testing" + + "github.com/stellar/wallet-backend/internal/db" + "github.com/stellar/wallet-backend/internal/db/dbtest" + "github.com/stellar/wallet-backend/internal/entities" + "github.com/stellar/wallet-backend/internal/tss" + "github.com/stellar/wallet-backend/internal/tss/router" + "github.com/stellar/wallet-backend/internal/tss/services" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +func TestNonJitterSend(t *testing.T) { + dbt := dbtest.Open(t) + defer dbt.Close() + dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) + require.NoError(t, err) + defer dbConnectionPool.Close() + txManagerMock := services.TransactionManagerMock{} + routerMock := router.MockRouter{} + cfg := ErrorNonJitterChannelConfigs{ + TxManager: &txManagerMock, + Router: &routerMock, + MaxBufferSize: 1, + MaxWorkers: 1, + MaxRetries: 3, + WaitBtwnRetriesMS: 10, + } + + channel := NewErrorNonJitterChannel(cfg) + + payload := tss.Payload{} + payload.WebhookURL = "www.stellar.com" + payload.TransactionHash = "hash" + payload.TransactionXDR = "xdr" + + rpcResp := tss.RPCSendTxResponse{ + Status: tss.RPCTXStatus{RPCStatus: entities.ErrorStatus}, + Code: tss.RPCTXCode{TxResultCode: tss.JitterErrorCodes[0]}, + } + payload.RpcSubmitTxResponse = rpcResp + + txManagerMock. + On("BuildAndSubmitTransaction", context.Background(), ErrorNonJitterChannelName, payload). + Return(rpcResp, nil). + Once() + + routerMock. + On("Route", payload). + Return(nil). + Once() + + channel.Send(payload) + channel.Stop() + + routerMock.AssertCalled(t, "Route", payload) +} + +func TestNonJitterReceive(t *testing.T) { + dbt := dbtest.Open(t) + defer dbt.Close() + dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) + require.NoError(t, err) + defer dbConnectionPool.Close() + txManagerMock := services.TransactionManagerMock{} + routerMock := router.MockRouter{} + cfg := ErrorNonJitterChannelConfigs{ + TxManager: &txManagerMock, + Router: &routerMock, + MaxBufferSize: 1, + MaxWorkers: 1, + MaxRetries: 3, + WaitBtwnRetriesMS: 10, + } + + channel := NewErrorNonJitterChannel(cfg) + payload := tss.Payload{} + payload.WebhookURL = "www.stellar.com" + payload.TransactionHash = "hash" + payload.TransactionXDR = "xdr" + t.Run("build_and_submit_tx_fail", func(t *testing.T) { + txManagerMock. + On("BuildAndSubmitTransaction", context.Background(), ErrorNonJitterChannelName, payload). + Return(tss.RPCSendTxResponse{}, errors.New("build tx failed")). + Once() + + channel.Receive(payload) + + routerMock.AssertNotCalled(t, "Route", payload) + }) + + t.Run("retries", func(t *testing.T) { + sendResp1 := tss.RPCSendTxResponse{ + Status: tss.RPCTXStatus{RPCStatus: entities.ErrorStatus}, + Code: tss.RPCTXCode{TxResultCode: tss.NonJitterErrorCodes[0]}, + } + sendResp2 := tss.RPCSendTxResponse{ + Status: tss.RPCTXStatus{RPCStatus: entities.ErrorStatus}, + Code: tss.RPCTXCode{TxResultCode: tss.JitterErrorCodes[0]}, + } + + txManagerMock. + On("BuildAndSubmitTransaction", context.Background(), ErrorNonJitterChannelName, mock.AnythingOfType("tss.Payload")). + Return(sendResp1, nil). + Once() + txManagerMock. + On("BuildAndSubmitTransaction", context.Background(), ErrorNonJitterChannelName, mock.AnythingOfType("tss.Payload")). + Return(sendResp2, nil). + Once() + routerMock. + On("Route", mock.AnythingOfType("tss.Payload")). + Return(nil). + Once() + + channel.Receive(payload) + }) + + t.Run("max_retries", func(t *testing.T) { + sendResp := tss.RPCSendTxResponse{ + Status: tss.RPCTXStatus{RPCStatus: entities.ErrorStatus}, + Code: tss.RPCTXCode{TxResultCode: tss.NonJitterErrorCodes[0]}, + } + + txManagerMock. + On("BuildAndSubmitTransaction", context.Background(), ErrorNonJitterChannelName, mock.AnythingOfType("tss.Payload")). + Return(sendResp, nil). + Times(3) + routerMock. + On("Route", mock.AnythingOfType("tss.Payload")). + Return(nil). + Once() + + channel.Receive(payload) + }) +} diff --git a/internal/tss/channels/error_service_handler_jitter_channel_test.go b/internal/tss/channels/error_service_handler_jitter_channel_test.go deleted file mode 100644 index bb4077d..0000000 --- a/internal/tss/channels/error_service_handler_jitter_channel_test.go +++ /dev/null @@ -1,224 +0,0 @@ -package channels - -import ( - "context" - "errors" - "testing" - - "github.com/stellar/go/xdr" - "github.com/stellar/wallet-backend/internal/db" - "github.com/stellar/wallet-backend/internal/db/dbtest" - "github.com/stellar/wallet-backend/internal/tss" - "github.com/stellar/wallet-backend/internal/tss/router" - "github.com/stellar/wallet-backend/internal/tss/store" - "github.com/stellar/wallet-backend/internal/tss/utils" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" -) - -func TestJitterSend(t *testing.T) { - dbt := dbtest.Open(t) - defer dbt.Close() - - dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) - require.NoError(t, err) - defer dbConnectionPool.Close() - store := store.NewStore(dbConnectionPool) - txServiceMock := utils.TransactionServiceMock{} - cfg := RPCErrorHandlerServiceJitterChannelConfigs{ - Store: store, - TxService: &txServiceMock, - MaxBufferSize: 1, - MaxWorkers: 1, - MaxRetries: 3, - MinWaitBtwnRetriesMS: 10, - } - channel := NewErrorHandlerServiceJitterChannel(cfg) - - payload := tss.Payload{} - payload.WebhookURL = "www.stellar.com" - payload.TransactionHash = "hash" - payload.TransactionXDR = "xdr" - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(nil, errors.New("signing failed")) - - _ = store.UpsertTransaction(context.Background(), payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.NewStatus) - - channel.Send(payload) - channel.Stop() - - var status string - err = dbConnectionPool.GetContext(context.Background(), &status, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, status, string(tss.NewStatus)) -} - -func TestJitterReceive(t *testing.T) { - dbt := dbtest.Open(t) - defer dbt.Close() - - dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) - require.NoError(t, err) - defer dbConnectionPool.Close() - store := store.NewStore(dbConnectionPool) - txServiceMock := utils.TransactionServiceMock{} - cfg := RPCErrorHandlerServiceJitterChannelConfigs{ - Store: store, - TxService: &txServiceMock, - MaxBufferSize: 1, - MaxWorkers: 1, - MaxRetries: 3, - MinWaitBtwnRetriesMS: 10, - } - channel := NewErrorHandlerServiceJitterChannel(cfg) - - mockRouter := router.MockRouter{} - defer mockRouter.AssertExpectations(t) - channel.SetRouter(&mockRouter) - networkPass := "passphrase" - feeBumpTx := utils.BuildTestFeeBumpTransaction() - feeBumpTxXDR, _ := feeBumpTx.Base64() - feeBumpTxHash, _ := feeBumpTx.HashHex(networkPass) - payload := tss.Payload{} - payload.WebhookURL = "www.stellar.com" - payload.TransactionHash = "hash" - payload.TransactionXDR = "xdr" - - _ = store.UpsertTransaction(context.Background(), payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.NewStatus) - - t.Run("signing_and_submitting_tx_fails", func(t *testing.T) { - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(nil, errors.New("sign tx failed")). - Once() - - channel.Receive(payload) - - var txStatus string - err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, string(tss.NewStatus), txStatus) - }) - - t.Run("payload_gets_routed", func(t *testing.T) { - sendResp := tss.RPCSendTxResponse{} - sendResp.Status = tss.ErrorStatus - sendResp.TransactionHash = feeBumpTxHash - sendResp.TransactionXDR = feeBumpTxXDR - sendResp.Code.TxResultCode = xdr.TransactionResultCodeTxTooEarly - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(feeBumpTx, nil). - Once(). - On("NetworkPassphrase"). - Return(networkPass). - Once(). - On("SendTransaction", feeBumpTxXDR). - Return(sendResp, nil). - Once() - - mockRouter. - On("Route", mock.AnythingOfType("tss.Payload")). - Return(). - Once() - - channel.Receive(payload) - - var txStatus string - err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, string(tss.ErrorStatus), txStatus) - - var tryStatus int - err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) - require.NoError(t, err) - assert.Equal(t, int(xdr.TransactionResultCodeTxTooEarly), tryStatus) - }) - - t.Run("retries", func(t *testing.T) { - sendResp1 := tss.RPCSendTxResponse{} - sendResp1.Status = tss.ErrorStatus - sendResp1.TransactionHash = feeBumpTxHash - sendResp1.TransactionXDR = feeBumpTxXDR - sendResp1.Code.TxResultCode = xdr.TransactionResultCodeTxInsufficientFee - - sendResp2 := tss.RPCSendTxResponse{} - sendResp2.Status = tss.FailedStatus - sendResp2.TransactionHash = feeBumpTxHash - sendResp2.TransactionXDR = feeBumpTxXDR - sendResp2.Code.TxResultCode = xdr.TransactionResultCodeTxFailed - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(feeBumpTx, nil). - Twice(). - On("NetworkPassphrase"). - Return(networkPass). - Twice() - - txServiceMock. - On("SendTransaction", feeBumpTxXDR). - Return(sendResp1, nil). - Once() - - txServiceMock. - On("SendTransaction", feeBumpTxXDR). - Return(sendResp2, nil). - Once() - - mockRouter. - On("Route", mock.AnythingOfType("tss.Payload")). - Return(). - Once() - - channel.Receive(payload) - - var txStatus string - err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, string(tss.FailedStatus), txStatus) - - var tryStatus int - err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) - require.NoError(t, err) - assert.Equal(t, int(xdr.TransactionResultCodeTxFailed), tryStatus) - }) - - t.Run("max_retries", func(t *testing.T) { - sendResp := tss.RPCSendTxResponse{} - sendResp.Status = tss.ErrorStatus - sendResp.TransactionHash = feeBumpTxHash - sendResp.TransactionXDR = feeBumpTxXDR - sendResp.Code.TxResultCode = xdr.TransactionResultCodeTxInsufficientFee - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(feeBumpTx, nil). - Times(3). - On("NetworkPassphrase"). - Return(networkPass). - Times(3) - - txServiceMock. - On("SendTransaction", feeBumpTxXDR). - Return(sendResp, nil). - Times(3) - - mockRouter. - On("Route", mock.AnythingOfType("tss.Payload")). - Return(). - Once() - - channel.Receive(payload) - - var txStatus string - err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, string(tss.ErrorStatus), txStatus) - - var tryStatus int - err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) - require.NoError(t, err) - assert.Equal(t, int(xdr.TransactionResultCodeTxInsufficientFee), tryStatus) - }) -} diff --git a/internal/tss/channels/rpc_caller_channel.go b/internal/tss/channels/rpc_caller_channel.go new file mode 100644 index 0000000..9a36318 --- /dev/null +++ b/internal/tss/channels/rpc_caller_channel.go @@ -0,0 +1,81 @@ +package channels + +import ( + "context" + + "github.com/alitto/pond" + + "github.com/stellar/go/support/log" + "github.com/stellar/wallet-backend/internal/entities" + "github.com/stellar/wallet-backend/internal/tss" + "github.com/stellar/wallet-backend/internal/tss/router" + "github.com/stellar/wallet-backend/internal/tss/services" + "github.com/stellar/wallet-backend/internal/tss/store" +) + +type RPCCallerChannelConfigs struct { + TxManager services.TransactionManager + Router router.Router + Store store.Store + MaxBufferSize int + MaxWorkers int +} + +type rpcCallerPool struct { + Pool *pond.WorkerPool + TxManager services.TransactionManager + Router router.Router + Store store.Store +} + +var RPCCallerChannelName = "RPCCallerChannel" + +func NewRPCCallerChannel(cfg RPCCallerChannelConfigs) *rpcCallerPool { + pool := pond.New(cfg.MaxBufferSize, cfg.MaxWorkers, pond.Strategy(pond.Balanced())) + return &rpcCallerPool{ + Pool: pool, + TxManager: cfg.TxManager, + Store: cfg.Store, + Router: cfg.Router, + } + +} + +func (p *rpcCallerPool) Send(payload tss.Payload) { + p.Pool.Submit(func() { + p.Receive(payload) + }) +} + +func (p *rpcCallerPool) Receive(payload tss.Payload) { + + ctx := context.Background() + // Create a new transaction record in the transactions table. + err := p.Store.UpsertTransaction(ctx, payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.RPCTXStatus{OtherStatus: tss.NewStatus}) + + if err != nil { + log.Errorf("%s: Unable to upsert transaction into transactions table: %e", RPCCallerChannelName, err) + return + } + rpcSendResp, err := p.TxManager.BuildAndSubmitTransaction(ctx, RPCCallerChannelName, payload) + + if err != nil { + log.Errorf("%s: Unable to sign and submit transaction: %e", RPCCallerChannelName, err) + return + } + payload.RpcSubmitTxResponse = rpcSendResp + if rpcSendResp.Status.RPCStatus == entities.TryAgainLaterStatus || rpcSendResp.Status.RPCStatus == entities.ErrorStatus { + err = p.Router.Route(payload) + if err != nil { + log.Errorf("%s: Unable to route payload: %e", RPCCallerChannelName, err) + } + } +} + +func (p *rpcCallerPool) SetRouter(router router.Router) { + p.Router = router +} + +func (p *rpcCallerPool) Stop() { + p.Pool.StopAndWait() +} diff --git a/internal/tss/channels/rpc_caller_channel_test.go b/internal/tss/channels/rpc_caller_channel_test.go new file mode 100644 index 0000000..b5899c0 --- /dev/null +++ b/internal/tss/channels/rpc_caller_channel_test.go @@ -0,0 +1,130 @@ +package channels + +import ( + "context" + "errors" + "testing" + + "github.com/stellar/wallet-backend/internal/db" + "github.com/stellar/wallet-backend/internal/db/dbtest" + "github.com/stellar/wallet-backend/internal/entities" + "github.com/stellar/wallet-backend/internal/tss" + "github.com/stellar/wallet-backend/internal/tss/router" + "github.com/stellar/wallet-backend/internal/tss/services" + "github.com/stellar/wallet-backend/internal/tss/store" + "github.com/stretchr/testify/require" +) + +func TestSend(t *testing.T) { + dbt := dbtest.Open(t) + defer dbt.Close() + dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) + require.NoError(t, err) + defer dbConnectionPool.Close() + store := store.NewStore(dbConnectionPool) + txManagerMock := services.TransactionManagerMock{} + routerMock := router.MockRouter{} + cfgs := RPCCallerChannelConfigs{ + Store: store, + TxManager: &txManagerMock, + Router: &routerMock, + MaxBufferSize: 10, + MaxWorkers: 10, + } + channel := NewRPCCallerChannel(cfgs) + payload := tss.Payload{} + payload.WebhookURL = "www.stellar.com" + payload.TransactionHash = "hash" + payload.TransactionXDR = "xdr" + + rpcResp := tss.RPCSendTxResponse{ + Status: tss.RPCTXStatus{RPCStatus: entities.TryAgainLaterStatus}, + } + payload.RpcSubmitTxResponse = rpcResp + + txManagerMock. + On("BuildAndSubmitTransaction", context.Background(), RPCCallerChannelName, payload). + Return(rpcResp, nil). + Once() + + routerMock. + On("Route", payload). + Return(nil). + Once() + + channel.Send(payload) + channel.Stop() + + routerMock.AssertCalled(t, "Route", payload) +} + +func TestReceivee(t *testing.T) { + dbt := dbtest.Open(t) + defer dbt.Close() + dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) + require.NoError(t, err) + defer dbConnectionPool.Close() + store := store.NewStore(dbConnectionPool) + txManagerMock := services.TransactionManagerMock{} + routerMock := router.MockRouter{} + cfgs := RPCCallerChannelConfigs{ + Store: store, + TxManager: &txManagerMock, + Router: &routerMock, + MaxBufferSize: 10, + MaxWorkers: 10, + } + channel := NewRPCCallerChannel(cfgs) + payload := tss.Payload{} + payload.WebhookURL = "www.stellar.com" + payload.TransactionHash = "hash" + payload.TransactionXDR = "xdr" + + t.Run("build_and_submit_tx_fail", func(t *testing.T) { + txManagerMock. + On("BuildAndSubmitTransaction", context.Background(), RPCCallerChannelName, payload). + Return(tss.RPCSendTxResponse{}, errors.New("build tx failed")). + Once() + + channel.Receive(payload) + + routerMock.AssertNotCalled(t, "Route", payload) + }) + + t.Run("payload_not_routed", func(t *testing.T) { + rpcResp := tss.RPCSendTxResponse{ + Status: tss.RPCTXStatus{RPCStatus: entities.PendingStatus}, + } + payload.RpcSubmitTxResponse = rpcResp + + txManagerMock. + On("BuildAndSubmitTransaction", context.Background(), RPCCallerChannelName, payload). + Return(rpcResp, nil). + Once() + + channel.Receive(payload) + + routerMock.AssertNotCalled(t, "Route", payload) + }) + t.Run("payload_routed", func(t *testing.T) { + rpcResp := tss.RPCSendTxResponse{ + Status: tss.RPCTXStatus{RPCStatus: entities.ErrorStatus}, + } + payload.RpcSubmitTxResponse = rpcResp + + txManagerMock. + On("BuildAndSubmitTransaction", context.Background(), RPCCallerChannelName, payload). + Return(rpcResp, nil). + Once() + + routerMock. + On("Route", payload). + Return(nil). + Once() + + channel.Receive(payload) + + routerMock.AssertCalled(t, "Route", payload) + }) + +} diff --git a/internal/tss/channels/rpc_caller_service_channel.go b/internal/tss/channels/rpc_caller_service_channel.go deleted file mode 100644 index 33300c3..0000000 --- a/internal/tss/channels/rpc_caller_service_channel.go +++ /dev/null @@ -1,78 +0,0 @@ -package channels - -import ( - "context" - - "github.com/alitto/pond" - - "github.com/stellar/go/support/log" - "github.com/stellar/wallet-backend/internal/tss" - "github.com/stellar/wallet-backend/internal/tss/router" - "github.com/stellar/wallet-backend/internal/tss/services" - "github.com/stellar/wallet-backend/internal/tss/store" - "github.com/stellar/wallet-backend/internal/tss/utils" -) - -type RPCCallerServiceChannelConfigs struct { - Store store.Store - TxService utils.TransactionService - Router router.Router - MaxBufferSize int - MaxWorkers int -} - -type rpcCallerServicePool struct { - Pool *pond.WorkerPool - TxService utils.TransactionService - ErrHandlerService services.Service - Store store.Store - Router router.Router -} - -func NewRPCCallerServiceChannel(cfg RPCCallerServiceChannelConfigs) *rpcCallerServicePool { - pool := pond.New(cfg.MaxBufferSize, cfg.MaxWorkers, pond.Strategy(pond.Balanced())) - return &rpcCallerServicePool{ - Pool: pool, - TxService: cfg.TxService, - Store: cfg.Store, - Router: cfg.Router, - } - -} - -func (p *rpcCallerServicePool) Send(payload tss.Payload) { - p.Pool.Submit(func() { - p.Receive(payload) - }) -} - -func (p *rpcCallerServicePool) Receive(payload tss.Payload) { - - ctx := context.Background() - // Create a new transaction record in the transactions table. - err := p.Store.UpsertTransaction(ctx, payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.NewStatus) - - if err != nil { - log.Errorf("Unable to upsert transaction into transactions table: %s", err.Error()) - return - } - - rpcSendResp, err := BuildAndSubmitTransaction(ctx, "RPCCallerServiceChannel", payload, p.Store, p.TxService) - - if err != nil { - log.Errorf(": Unable to sign and submit transaction: %s", err.Error()) - return - } - payload.RpcSubmitTxResponse = rpcSendResp - if rpcSendResp.Status == tss.TryAgainLaterStatus || rpcSendResp.Status == tss.ErrorStatus { - p.Router.Route(payload) - } -} - -func (p *rpcCallerServicePool) SetRouter(router router.Router) { - p.Router = router -} - -func (p *rpcCallerServicePool) Stop() { - p.Pool.StopAndWait() -} diff --git a/internal/tss/channels/rpc_caller_service_channel_test.go b/internal/tss/channels/rpc_caller_service_channel_test.go deleted file mode 100644 index 797b56d..0000000 --- a/internal/tss/channels/rpc_caller_service_channel_test.go +++ /dev/null @@ -1,207 +0,0 @@ -package channels - -import ( - "context" - "errors" - "testing" - - "github.com/stellar/go/xdr" - "github.com/stellar/wallet-backend/internal/db" - "github.com/stellar/wallet-backend/internal/db/dbtest" - "github.com/stellar/wallet-backend/internal/tss" - "github.com/stellar/wallet-backend/internal/tss/router" - "github.com/stellar/wallet-backend/internal/tss/store" - "github.com/stellar/wallet-backend/internal/tss/utils" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "github.com/stretchr/testify/require" -) - -func TestSend(t *testing.T) { - dbt := dbtest.Open(t) - defer dbt.Close() - dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) - require.NoError(t, err) - defer dbConnectionPool.Close() - store := store.NewStore(dbConnectionPool) - txServiceMock := utils.TransactionServiceMock{} - cfgs := RPCCallerServiceChannelConfigs{ - Store: store, - TxService: &txServiceMock, - MaxBufferSize: 10, - MaxWorkers: 10, - } - channel := NewRPCCallerServiceChannel(cfgs) - payload := tss.Payload{} - payload.WebhookURL = "www.stellar.com" - payload.TransactionHash = "hash" - payload.TransactionXDR = "xdr" - networkPass := "passphrase" - - feeBumpTx := utils.BuildTestFeeBumpTransaction() - feeBumpTxXDR, _ := feeBumpTx.Base64() - sendResp := tss.RPCSendTxResponse{} - sendResp.Code.OtherCodes = tss.RPCFailCode - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(feeBumpTx, nil). - Once(). - On("NetworkPassphrase"). - Return(networkPass). - Once(). - On("SendTransaction", feeBumpTxXDR). - Return(sendResp, errors.New("RPC Fail")). - Once() - - channel.Send(payload) - channel.Stop() - - var status string - err = dbConnectionPool.GetContext(context.Background(), &status, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, status, string(tss.NewStatus)) - - var tryStatus int - feeBumpTxHash, _ := feeBumpTx.HashHex(networkPass) - err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) - require.NoError(t, err) - assert.Equal(t, int(tss.RPCFailCode), tryStatus) -} - -func TestReceive(t *testing.T) { - dbt := dbtest.Open(t) - defer dbt.Close() - - dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) - require.NoError(t, err) - defer dbConnectionPool.Close() - store := store.NewStore(dbConnectionPool) - txServiceMock := utils.TransactionServiceMock{} - defer txServiceMock.AssertExpectations(t) - routerMock := router.MockRouter{} - defer routerMock.AssertExpectations(t) - cfgs := RPCCallerServiceChannelConfigs{ - Store: store, - TxService: &txServiceMock, - Router: &routerMock, - MaxBufferSize: 1, - MaxWorkers: 1, - } - networkPass := "passphrase" - channel := NewRPCCallerServiceChannel(cfgs) - feeBumpTx := utils.BuildTestFeeBumpTransaction() - feeBumpTxXDR, _ := feeBumpTx.Base64() - feeBumpTxHash, _ := feeBumpTx.HashHex(networkPass) - payload := tss.Payload{} - payload.WebhookURL = "www.stellar.com" - payload.TransactionHash = "hash" - payload.TransactionXDR = "xdr" - - t.Run("fail_on_tx_build_and_sign", func(t *testing.T) { - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(nil, errors.New("signing failed")). - Once() - channel.Receive(payload) - - var status string - err = dbConnectionPool.GetContext(context.Background(), &status, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, string(tss.NewStatus), status) - }) - - t.Run("sign_and_submit_tx_fails", func(t *testing.T) { - sendResp := tss.RPCSendTxResponse{} - sendResp.Code.OtherCodes = tss.RPCFailCode - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(feeBumpTx, nil). - Once(). - On("NetworkPassphrase"). - Return(networkPass). - Once(). - On("SendTransaction", feeBumpTxXDR). - Return(sendResp, errors.New("RPC Fail")). - Once() - - channel.Receive(payload) - - var txStatus string - err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, txStatus, string(tss.NewStatus)) - - var tryStatus int - feeBumpTxHash, _ := feeBumpTx.HashHex(networkPass) - err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) - require.NoError(t, err) - assert.Equal(t, int(tss.RPCFailCode), tryStatus) - - }) - - t.Run("routes_payload", func(t *testing.T) { - sendResp := tss.RPCSendTxResponse{} - sendResp.Status = tss.ErrorStatus - sendResp.TransactionHash = feeBumpTxHash - sendResp.TransactionXDR = feeBumpTxXDR - sendResp.Code.TxResultCode = xdr.TransactionResultCodeTxTooEarly - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(feeBumpTx, nil). - Once(). - On("NetworkPassphrase"). - Return(networkPass). - Once(). - On("SendTransaction", feeBumpTxXDR). - Return(sendResp, nil). - Once() - routerMock. - On("Route", mock.AnythingOfType("tss.Payload")). - Return(). - Once() - - channel.Receive(payload) - - var txStatus string - err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, string(tss.ErrorStatus), txStatus) - - var tryStatus int - err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) - require.NoError(t, err) - assert.Equal(t, int(xdr.TransactionResultCodeTxTooEarly), tryStatus) - }) - - t.Run("does_not_routes_payload", func(t *testing.T) { - sendResp := tss.RPCSendTxResponse{} - sendResp.Status = tss.PendingStatus - sendResp.TransactionHash = feeBumpTxHash - sendResp.TransactionXDR = feeBumpTxXDR - sendResp.Code.TxResultCode = xdr.TransactionResultCodeTxSuccess - txServiceMock. - On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). - Return(feeBumpTx, nil). - Once(). - On("NetworkPassphrase"). - Return(networkPass). - Once(). - On("SendTransaction", feeBumpTxXDR). - Return(sendResp, nil). - Once() - // this time the router mock is not called - - channel.Receive(payload) - - var txStatus string - err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, string(tss.PendingStatus), txStatus) - - var tryStatus int - err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) - require.NoError(t, err) - assert.Equal(t, int(xdr.TransactionResultCodeTxSuccess), tryStatus) - }) - -} diff --git a/internal/tss/channels/utils.go b/internal/tss/channels/utils.go deleted file mode 100644 index 3da7d9a..0000000 --- a/internal/tss/channels/utils.go +++ /dev/null @@ -1,54 +0,0 @@ -package channels - -import ( - "fmt" - "time" - - "github.com/stellar/wallet-backend/internal/tss" - "github.com/stellar/wallet-backend/internal/tss/store" - "github.com/stellar/wallet-backend/internal/tss/utils" - "golang.org/x/exp/rand" - "golang.org/x/net/context" -) - -func jitter(dur time.Duration) time.Duration { - halfDur := int64(dur / 2) - delta := rand.Int63n(halfDur) - halfDur/2 - return dur + time.Duration(delta) -} - -func BuildAndSubmitTransaction(ctx context.Context, channelName string, payload tss.Payload, store store.Store, txService utils.TransactionService) (tss.RPCSendTxResponse, error) { - feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(ctx, payload.TransactionXDR) - if err != nil { - return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to sign/build transaction: %s", channelName, err.Error()) - } - feeBumpTxHash, err := feeBumpTx.HashHex(txService.NetworkPassphrase()) - if err != nil { - return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to hashhex fee bump transaction: %s", channelName, err.Error()) - } - - feeBumpTxXDR, err := feeBumpTx.Base64() - if err != nil { - return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to base64 fee bump transaction: %s", channelName, err.Error()) - } - - err = store.UpsertTry(ctx, payload.TransactionHash, feeBumpTxHash, feeBumpTxXDR, tss.RPCTXCode{OtherCodes: tss.NewCode}) - if err != nil { - return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to upsert try in tries table: %s", channelName, err.Error()) - } - rpcSendResp, rpcErr := txService.SendTransaction(feeBumpTxXDR) - - err = store.UpsertTry(ctx, payload.TransactionHash, feeBumpTxHash, feeBumpTxXDR, rpcSendResp.Code) - if err != nil { - return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to upsert try in tries table: %s", channelName, err.Error()) - } - if rpcErr != nil && rpcSendResp.Code.OtherCodes == tss.RPCFailCode || rpcSendResp.Code.OtherCodes == tss.UnMarshalBinaryCode { - return tss.RPCSendTxResponse{}, fmt.Errorf("%s: RPC fail: %s", channelName, rpcErr.Error()) - } - - err = store.UpsertTransaction(ctx, payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, rpcSendResp.Status) - if err != nil { - return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to do the final update of tx in the transactions table: %s", channelName, err.Error()) - } - return rpcSendResp, nil -} diff --git a/internal/tss/channels/webhook_handler_service_channel.go b/internal/tss/channels/webhook_channel.go similarity index 71% rename from internal/tss/channels/webhook_handler_service_channel.go rename to internal/tss/channels/webhook_channel.go index 49ddde5..31efae5 100644 --- a/internal/tss/channels/webhook_handler_service_channel.go +++ b/internal/tss/channels/webhook_channel.go @@ -9,10 +9,11 @@ import ( "github.com/alitto/pond" "github.com/stellar/go/support/log" "github.com/stellar/wallet-backend/internal/tss" - "github.com/stellar/wallet-backend/internal/tss/utils" + tssutils "github.com/stellar/wallet-backend/internal/tss/utils" + "github.com/stellar/wallet-backend/internal/utils" ) -type WebhookHandlerServiceChannelConfigs struct { +type WebhookChannelConfigs struct { HTTPClient utils.HTTPClient MaxBufferSize int MaxWorkers int @@ -20,18 +21,18 @@ type WebhookHandlerServiceChannelConfigs struct { MinWaitBtwnRetriesMS int } -type webhookHandlerServicePool struct { +type webhookPool struct { Pool *pond.WorkerPool HTTPClient utils.HTTPClient MaxRetries int MinWaitBtwnRetriesMS int } -var _ tss.Channel = (*webhookHandlerServicePool)(nil) +var _ tss.Channel = (*webhookPool)(nil) -func NewWebhookHandlerServiceChannel(cfg WebhookHandlerServiceChannelConfigs) *webhookHandlerServicePool { +func NewWebhookChannel(cfg WebhookChannelConfigs) *webhookPool { pool := pond.New(cfg.MaxBufferSize, cfg.MaxWorkers, pond.Strategy(pond.Balanced())) - return &webhookHandlerServicePool{ + return &webhookPool{ Pool: pool, HTTPClient: cfg.HTTPClient, MaxRetries: cfg.MaxRetries, @@ -40,14 +41,14 @@ func NewWebhookHandlerServiceChannel(cfg WebhookHandlerServiceChannelConfigs) *w } -func (p *webhookHandlerServicePool) Send(payload tss.Payload) { +func (p *webhookPool) Send(payload tss.Payload) { p.Pool.Submit(func() { p.Receive(payload) }) } -func (p *webhookHandlerServicePool) Receive(payload tss.Payload) { - resp := utils.PayloadTOTSSResponse(payload) +func (p *webhookPool) Receive(payload tss.Payload) { + resp := tssutils.PayloadTOTSSResponse(payload) jsonData, err := json.Marshal(resp) if err != nil { log.Errorf("WebhookHandlerServiceChannel: error marshaling payload: %s", err.Error()) @@ -69,6 +70,6 @@ func (p *webhookHandlerServicePool) Receive(payload tss.Payload) { } } -func (p *webhookHandlerServicePool) Stop() { +func (p *webhookPool) Stop() { p.Pool.StopAndWait() } diff --git a/internal/tss/channels/webhook_handler_service_channel_test.go b/internal/tss/channels/webhook_channel_test.go similarity index 81% rename from internal/tss/channels/webhook_handler_service_channel_test.go rename to internal/tss/channels/webhook_channel_test.go index df391e4..88e4cf3 100644 --- a/internal/tss/channels/webhook_handler_service_channel_test.go +++ b/internal/tss/channels/webhook_channel_test.go @@ -9,23 +9,24 @@ import ( "testing" "github.com/stellar/wallet-backend/internal/tss" - "github.com/stellar/wallet-backend/internal/tss/utils" + tssutils "github.com/stellar/wallet-backend/internal/tss/utils" + "github.com/stellar/wallet-backend/internal/utils" ) func TestWebhookHandlerServiceChannel(t *testing.T) { mockHTTPClient := utils.MockHTTPClient{} - cfg := WebhookHandlerServiceChannelConfigs{ + cfg := WebhookChannelConfigs{ HTTPClient: &mockHTTPClient, MaxBufferSize: 1, MaxWorkers: 1, MaxRetries: 3, MinWaitBtwnRetriesMS: 5, } - channel := NewWebhookHandlerServiceChannel(cfg) + channel := NewWebhookChannel(cfg) payload := tss.Payload{} payload.WebhookURL = "www.stellar.org" - jsonData, _ := json.Marshal(utils.PayloadTOTSSResponse(payload)) + jsonData, _ := json.Marshal(tssutils.PayloadTOTSSResponse(payload)) httpResponse1 := &http.Response{ StatusCode: http.StatusBadGateway, diff --git a/internal/tss/router/mocks.go b/internal/tss/router/mocks.go index 3f4406c..2f269b7 100644 --- a/internal/tss/router/mocks.go +++ b/internal/tss/router/mocks.go @@ -11,6 +11,7 @@ type MockRouter struct { var _ Router = (*MockRouter)(nil) -func (r *MockRouter) Route(payload tss.Payload) { - r.Called(payload) +func (r *MockRouter) Route(payload tss.Payload) error { + args := r.Called(payload) + return args.Error(0) } diff --git a/internal/tss/router/router.go b/internal/tss/router/router.go index dcbd2db..8c6801a 100644 --- a/internal/tss/router/router.go +++ b/internal/tss/router/router.go @@ -1,67 +1,68 @@ package router import ( + "fmt" "slices" - "github.com/stellar/go/xdr" + "github.com/stellar/wallet-backend/internal/entities" "github.com/stellar/wallet-backend/internal/tss" - "github.com/stellar/wallet-backend/internal/tss/services" ) type Router interface { - Route(payload tss.Payload) + Route(payload tss.Payload) error } type RouterConfigs struct { - ErrorHandlerService services.Service - WebhookHandlerService services.Service + RPCCallerChannel tss.Channel + ErrorJitterChannel tss.Channel + ErrorNonJitterChannel tss.Channel + WebhookChannel tss.Channel } type router struct { - ErrorHandlerService services.Service - WebhookHandlerService services.Service + RPCCallerChannel tss.Channel + ErrorJitterChannel tss.Channel + ErrorNonJitterChannel tss.Channel + WebhookChannel tss.Channel } var _ Router = (*router)(nil) -var FinalErrorCodes = []xdr.TransactionResultCode{ - xdr.TransactionResultCodeTxSuccess, - xdr.TransactionResultCodeTxFailed, - xdr.TransactionResultCodeTxMissingOperation, - xdr.TransactionResultCodeTxInsufficientBalance, - xdr.TransactionResultCodeTxBadAuthExtra, - xdr.TransactionResultCodeTxMalformed, -} - -var RetryErrorCodes = []xdr.TransactionResultCode{ - xdr.TransactionResultCodeTxTooLate, - xdr.TransactionResultCodeTxInsufficientFee, - xdr.TransactionResultCodeTxInternalError, - xdr.TransactionResultCodeTxBadSeq, -} - func NewRouter(cfg RouterConfigs) Router { return &router{ - ErrorHandlerService: cfg.ErrorHandlerService, - WebhookHandlerService: cfg.WebhookHandlerService, + RPCCallerChannel: cfg.RPCCallerChannel, + ErrorJitterChannel: cfg.ErrorJitterChannel, + ErrorNonJitterChannel: cfg.ErrorNonJitterChannel, + WebhookChannel: cfg.WebhookChannel, } } -func (r *router) Route(payload tss.Payload) { - switch payload.RpcSubmitTxResponse.Status { - case tss.TryAgainLaterStatus: - r.ErrorHandlerService.ProcessPayload(payload) - case tss.ErrorStatus: - if payload.RpcSubmitTxResponse.Code.OtherCodes == tss.NoCode { - if slices.Contains(RetryErrorCodes, payload.RpcSubmitTxResponse.Code.TxResultCode) { - r.ErrorHandlerService.ProcessPayload(payload) - } else if slices.Contains(FinalErrorCodes, payload.RpcSubmitTxResponse.Code.TxResultCode) { - r.WebhookHandlerService.ProcessPayload(payload) +func (r *router) Route(payload tss.Payload) error { + var channel tss.Channel + if payload.RpcSubmitTxResponse.Status.Status() != "" { + switch payload.RpcSubmitTxResponse.Status.Status() { + case string(tss.NewStatus): + channel = r.RPCCallerChannel + case string(entities.TryAgainLaterStatus): + channel = r.ErrorJitterChannel + case string(entities.ErrorStatus): + if payload.RpcSubmitTxResponse.Code.OtherCodes == tss.NoCode { + if slices.Contains(tss.JitterErrorCodes, payload.RpcSubmitTxResponse.Code.TxResultCode) { + channel = r.ErrorJitterChannel + } else if slices.Contains(tss.NonJitterErrorCodes, payload.RpcSubmitTxResponse.Code.TxResultCode) { + channel = r.ErrorNonJitterChannel + } else if slices.Contains(tss.FinalErrorCodes, payload.RpcSubmitTxResponse.Code.TxResultCode) { + channel = r.WebhookChannel + } } + default: + // Do nothing for PENDING / DUPLICATE statuses + return nil } - // if Code.OtherCodes = {RPCFailCode, UnMarshall, do nothing, as this should be rare. Let the ticker task take care of this} - default: - // PENDING = wait to ingest this transaction via getTransactions() - return } + if channel == nil { + return fmt.Errorf("payload could not be routed - channel is nil") + } + channel.Send(payload) + return nil } diff --git a/internal/tss/router/router_test.go b/internal/tss/router/router_test.go index 8a5907a..58eea6f 100644 --- a/internal/tss/router/router_test.go +++ b/internal/tss/router/router_test.go @@ -3,49 +3,129 @@ package router import ( "testing" - "github.com/stellar/go/xdr" + "github.com/stellar/wallet-backend/internal/entities" "github.com/stellar/wallet-backend/internal/tss" - "github.com/stellar/wallet-backend/internal/tss/services" + "github.com/stretchr/testify/assert" ) func TestRouter(t *testing.T) { - errorHandlerService := services.MockService{} - defer errorHandlerService.AssertExpectations(t) - webhookHandlerService := services.MockService{} - router := NewRouter(RouterConfigs{ErrorHandlerService: &errorHandlerService, WebhookHandlerService: &webhookHandlerService}) - t.Run("status_try_again_later", func(t *testing.T) { + rpcCallerChannel := tss.MockChannel{} + defer rpcCallerChannel.AssertExpectations(t) + errorJitterChannel := tss.MockChannel{} + defer errorJitterChannel.AssertExpectations(t) + errorNonJitterChannel := tss.MockChannel{} + defer errorNonJitterChannel.AssertExpectations(t) + webhookChannel := tss.MockChannel{} + defer webhookChannel.AssertExpectations(t) + + router := NewRouter(RouterConfigs{ + RPCCallerChannel: &rpcCallerChannel, + ErrorJitterChannel: &errorJitterChannel, + ErrorNonJitterChannel: &errorNonJitterChannel, + WebhookChannel: &webhookChannel, + }) + t.Run("status_new_routes_to_rpc_caller_channel", func(t *testing.T) { payload := tss.Payload{} - payload.RpcSubmitTxResponse.Status = tss.TryAgainLaterStatus + payload.RpcSubmitTxResponse.Status = tss.RPCTXStatus{OtherStatus: tss.NewStatus} - errorHandlerService. - On("ProcessPayload", payload). + rpcCallerChannel. + On("Send", payload). Return(). Once() - router.Route(payload) + _ = router.Route(payload) + + rpcCallerChannel.AssertCalled(t, "Send", payload) }) - t.Run("error_status_route_to_error_handler_service", func(t *testing.T) { + t.Run("status_try_again_later_routes_to_error_jitter_channel", func(t *testing.T) { payload := tss.Payload{} - payload.RpcSubmitTxResponse.Status = tss.ErrorStatus - payload.RpcSubmitTxResponse.Code.TxResultCode = xdr.TransactionResultCodeTxInsufficientFee + payload.RpcSubmitTxResponse.Status = tss.RPCTXStatus{RPCStatus: entities.TryAgainLaterStatus} - errorHandlerService. - On("ProcessPayload", payload). + errorJitterChannel. + On("Send", payload). Return(). Once() - router.Route(payload) + _ = router.Route(payload) + + errorJitterChannel.AssertCalled(t, "Send", payload) + }) + t.Run("status_error_routes_to_error_jitter_channel", func(t *testing.T) { + for _, code := range tss.JitterErrorCodes { + payload := tss.Payload{ + RpcSubmitTxResponse: tss.RPCSendTxResponse{ + Status: tss.RPCTXStatus{ + RPCStatus: entities.ErrorStatus, + }, + Code: tss.RPCTXCode{ + TxResultCode: code, + }, + }, + } + payload.RpcSubmitTxResponse.Code.TxResultCode = code + errorJitterChannel. + On("Send", payload). + Return(). + Once() + + _ = router.Route(payload) + + errorJitterChannel.AssertCalled(t, "Send", payload) + } + }) + t.Run("status_error_routes_to_error_non_jitter_channel", func(t *testing.T) { + + for _, code := range tss.NonJitterErrorCodes { + payload := tss.Payload{ + RpcSubmitTxResponse: tss.RPCSendTxResponse{ + Status: tss.RPCTXStatus{ + RPCStatus: entities.ErrorStatus, + }, + Code: tss.RPCTXCode{ + TxResultCode: code, + }, + }, + } + payload.RpcSubmitTxResponse.Code.TxResultCode = code + errorNonJitterChannel. + On("Send", payload). + Return(). + Once() + + _ = router.Route(payload) + + errorNonJitterChannel.AssertCalled(t, "Send", payload) + } + }) + t.Run("status_error_routes_to_webhook_channel", func(t *testing.T) { + for _, code := range tss.FinalErrorCodes { + payload := tss.Payload{ + RpcSubmitTxResponse: tss.RPCSendTxResponse{ + Status: tss.RPCTXStatus{ + RPCStatus: entities.ErrorStatus, + }, + Code: tss.RPCTXCode{ + TxResultCode: code, + }, + }, + } + payload.RpcSubmitTxResponse.Code.TxResultCode = code + webhookChannel. + On("Send", payload). + Return(). + Once() + + _ = router.Route(payload) + + webhookChannel.AssertCalled(t, "Send", payload) + } }) - t.Run("error_status_route_to_webhook_handler_service", func(t *testing.T) { + t.Run("nil_channel_does_not_route", func(t *testing.T) { payload := tss.Payload{} - payload.RpcSubmitTxResponse.Status = tss.ErrorStatus - payload.RpcSubmitTxResponse.Code.TxResultCode = xdr.TransactionResultCodeTxInsufficientBalance - webhookHandlerService. - On("ProcessPayload", payload). - Return(). - Once() + err := router.Route(payload) - router.Route(payload) + errorJitterChannel.AssertNotCalled(t, "Send", payload) + assert.Equal(t, "payload could not be routed - channel is nil", err.Error()) }) } diff --git a/internal/tss/services/error_handler_service.go b/internal/tss/services/error_handler_service.go deleted file mode 100644 index e24c310..0000000 --- a/internal/tss/services/error_handler_service.go +++ /dev/null @@ -1,36 +0,0 @@ -package services - -import ( - "slices" - - "github.com/stellar/wallet-backend/internal/tss" -) - -type errorHandlerService struct { - JitterChannel tss.Channel - NonJitterChannel tss.Channel -} - -type ErrorHandlerServiceConfigs struct { - JitterChannel tss.Channel - NonJitterChannel tss.Channel -} - -func NewErrorHandlerService(cfg ErrorHandlerServiceConfigs) *errorHandlerService { - return &errorHandlerService{ - JitterChannel: cfg.JitterChannel, - NonJitterChannel: cfg.NonJitterChannel, - } -} - -func (p *errorHandlerService) ProcessPayload(payload tss.Payload) { - if payload.RpcSubmitTxResponse.Status == tss.TryAgainLaterStatus { - p.JitterChannel.Send(payload) - } else { - if slices.Contains(tss.NonJitterErrorCodes, payload.RpcSubmitTxResponse.Code.TxResultCode) { - p.NonJitterChannel.Send(payload) - } else if slices.Contains(tss.JitterErrorCodes, payload.RpcSubmitTxResponse.Code.TxResultCode) { - p.JitterChannel.Send(payload) - } - } -} diff --git a/internal/tss/services/error_handler_service_test.go b/internal/tss/services/error_handler_service_test.go deleted file mode 100644 index 802cc8c..0000000 --- a/internal/tss/services/error_handler_service_test.go +++ /dev/null @@ -1,54 +0,0 @@ -package services - -import ( - "testing" - - "github.com/stellar/go/xdr" - "github.com/stellar/wallet-backend/internal/tss" -) - -func TestProcessPayload(t *testing.T) { - jitterChannel := tss.MockChannel{} - defer jitterChannel.AssertExpectations(t) - nonJitterChannel := tss.MockChannel{} - defer nonJitterChannel.AssertExpectations(t) - - service := NewErrorHandlerService(ErrorHandlerServiceConfigs{JitterChannel: &jitterChannel, NonJitterChannel: &nonJitterChannel}) - - t.Run("status_try_again_later", func(t *testing.T) { - payload := tss.Payload{} - payload.RpcSubmitTxResponse.Status = tss.TryAgainLaterStatus - - jitterChannel. - On("Send", payload). - Return(). - Once() - - service.ProcessPayload(payload) - }) - t.Run("code_tx_too_early", func(t *testing.T) { - payload := tss.Payload{} - payload.RpcSubmitTxResponse.Status = tss.ErrorStatus - payload.RpcSubmitTxResponse.Code.TxResultCode = xdr.TransactionResultCodeTxTooEarly - - nonJitterChannel. - On("Send", payload). - Return(). - Once() - - service.ProcessPayload(payload) - }) - - t.Run("code_tx_insufficient_fee", func(t *testing.T) { - payload := tss.Payload{} - payload.RpcSubmitTxResponse.Status = tss.ErrorStatus - payload.RpcSubmitTxResponse.Code.TxResultCode = xdr.TransactionResultCodeTxInsufficientFee - - jitterChannel. - On("Send", payload). - Return(). - Once() - - service.ProcessPayload(payload) - }) -} diff --git a/internal/tss/services/mocks.go b/internal/tss/services/mocks.go index fff8db8..3edcb26 100644 --- a/internal/tss/services/mocks.go +++ b/internal/tss/services/mocks.go @@ -1,16 +1,51 @@ package services import ( + "context" + + "github.com/stellar/go/txnbuild" "github.com/stellar/wallet-backend/internal/tss" + "github.com/stretchr/testify/mock" ) -type MockService struct { +type TransactionServiceMock struct { + mock.Mock +} + +var _ TransactionService = (*TransactionServiceMock)(nil) + +func (t *TransactionServiceMock) NetworkPassphrase() string { + args := t.Called() + return args.String(0) +} + +func (t *TransactionServiceMock) SignAndBuildNewFeeBumpTransaction(ctx context.Context, origTxXdr string) (*txnbuild.FeeBumpTransaction, error) { + args := t.Called(ctx, origTxXdr) + if result := args.Get(0); result != nil { + return result.(*txnbuild.FeeBumpTransaction), args.Error(1) + } + return nil, args.Error(1) + +} + +func (t *TransactionServiceMock) SendTransaction(transactionXdr string) (tss.RPCSendTxResponse, error) { + args := t.Called(transactionXdr) + return args.Get(0).(tss.RPCSendTxResponse), args.Error(1) +} + +func (t *TransactionServiceMock) GetTransaction(transactionHash string) (tss.RPCGetIngestTxResponse, error) { + args := t.Called(transactionHash) + return args.Get(0).(tss.RPCGetIngestTxResponse), args.Error(1) +} + +type TransactionManagerMock struct { mock.Mock } -var _ Service = (*MockService)(nil) +var _ TransactionManager = (*TransactionManagerMock)(nil) -func (s *MockService) ProcessPayload(payload tss.Payload) { - s.Called(payload) +func (t *TransactionManagerMock) BuildAndSubmitTransaction(ctx context.Context, channelName string, payload tss.Payload) (tss.RPCSendTxResponse, error) { + args := t.Called(ctx, channelName, payload) + return args.Get(0).(tss.RPCSendTxResponse), args.Error(1) } diff --git a/internal/tss/services/rpc_caller_service.go b/internal/tss/services/rpc_caller_service.go deleted file mode 100644 index 7f539eb..0000000 --- a/internal/tss/services/rpc_caller_service.go +++ /dev/null @@ -1,21 +0,0 @@ -package services - -import ( - "github.com/stellar/wallet-backend/internal/tss" -) - -type rpcCallerService struct { - channel tss.Channel -} - -var _ Service = (*rpcCallerService)(nil) - -func NewRPCCallerService(channel tss.Channel) Service { - return &rpcCallerService{ - channel: channel, - } -} - -func (p *rpcCallerService) ProcessPayload(payload tss.Payload) { - p.channel.Send(payload) -} diff --git a/internal/tss/services/transaction_manager.go b/internal/tss/services/transaction_manager.go new file mode 100644 index 0000000..7825582 --- /dev/null +++ b/internal/tss/services/transaction_manager.go @@ -0,0 +1,76 @@ +package services + +import ( + "context" + "fmt" + + "github.com/stellar/wallet-backend/internal/services" + "github.com/stellar/wallet-backend/internal/tss" + "github.com/stellar/wallet-backend/internal/tss/store" +) + +type TransactionManager interface { + BuildAndSubmitTransaction(ctx context.Context, channelName string, payload tss.Payload) (tss.RPCSendTxResponse, error) +} + +type TransactionManagerConfigs struct { + TxService TransactionService + RPCService services.RPCService + Store store.Store +} + +type transactionManager struct { + TxService TransactionService + RPCService services.RPCService + Store store.Store +} + +func NewTransactionManager(cfg TransactionManagerConfigs) *transactionManager { + return &transactionManager{ + TxService: cfg.TxService, + RPCService: cfg.RPCService, + Store: cfg.Store, + } +} + +func (t *transactionManager) BuildAndSubmitTransaction(ctx context.Context, channelName string, payload tss.Payload) (tss.RPCSendTxResponse, error) { + feeBumpTx, err := t.TxService.SignAndBuildNewFeeBumpTransaction(ctx, payload.TransactionXDR) + if err != nil { + return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to sign/build transaction: %w", channelName, err) + } + feeBumpTxHash, err := feeBumpTx.HashHex(t.TxService.NetworkPassphrase()) + if err != nil { + return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to hashhex fee bump transaction: %w", channelName, err) + } + + feeBumpTxXDR, err := feeBumpTx.Base64() + if err != nil { + return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to base64 fee bump transaction: %w", channelName, err) + } + + err = t.Store.UpsertTry(ctx, payload.TransactionHash, feeBumpTxHash, feeBumpTxXDR, tss.RPCTXCode{OtherCodes: tss.NewCode}) + if err != nil { + return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to upsert try in tries table: %w", channelName, err) + } + rpcResp, rpcErr := t.RPCService.SendTransaction(feeBumpTxXDR) + rpcSendResp, parseErr := tss.ParseToRPCSendTxResponse(feeBumpTxHash, rpcResp, rpcErr) + + err = t.Store.UpsertTry(ctx, payload.TransactionHash, feeBumpTxHash, feeBumpTxXDR, rpcSendResp.Code) + if err != nil { + return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to upsert try in tries table: %s", channelName, err.Error()) + } + + if parseErr != nil { + return rpcSendResp, fmt.Errorf("%s: RPC fail: %w", channelName, parseErr) + } + + if rpcErr != nil && rpcSendResp.Code.OtherCodes == tss.RPCFailCode || rpcSendResp.Code.OtherCodes == tss.UnmarshalBinaryCode { + return tss.RPCSendTxResponse{}, fmt.Errorf("%s: RPC fail: %w", channelName, rpcErr) + } + + err = t.Store.UpsertTransaction(ctx, payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, rpcSendResp.Status) + if err != nil { + return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to do the final update of tx in the transactions table: %s", channelName, err.Error()) + } + return rpcSendResp, nil +} diff --git a/internal/tss/channels/utils_test.go b/internal/tss/services/transaction_manager_test.go similarity index 50% rename from internal/tss/channels/utils_test.go rename to internal/tss/services/transaction_manager_test.go index c3dcb97..80731d4 100644 --- a/internal/tss/channels/utils_test.go +++ b/internal/tss/services/transaction_manager_test.go @@ -1,4 +1,4 @@ -package channels +package services import ( "context" @@ -8,6 +8,8 @@ import ( "github.com/stellar/go/xdr" "github.com/stellar/wallet-backend/internal/db" "github.com/stellar/wallet-backend/internal/db/dbtest" + "github.com/stellar/wallet-backend/internal/entities" + "github.com/stellar/wallet-backend/internal/services/servicesmocks" "github.com/stellar/wallet-backend/internal/tss" "github.com/stellar/wallet-backend/internal/tss/store" "github.com/stellar/wallet-backend/internal/tss/utils" @@ -23,7 +25,13 @@ func TestBuildAndSubmitTransaction(t *testing.T) { require.NoError(t, err) defer dbConnectionPool.Close() store := store.NewStore(dbConnectionPool) - txServiceMock := utils.TransactionServiceMock{} + txServiceMock := TransactionServiceMock{} + rpcServiceMock := servicesmocks.RPCServiceMock{} + txManager := NewTransactionManager(TransactionManagerConfigs{ + TxService: &txServiceMock, + RPCService: &rpcServiceMock, + Store: store, + }) networkPass := "passphrase" feeBumpTx := utils.BuildTestFeeBumpTransaction() feeBumpTxXDR, _ := feeBumpTx.Base64() @@ -33,14 +41,14 @@ func TestBuildAndSubmitTransaction(t *testing.T) { payload.TransactionHash = "hash" payload.TransactionXDR = "xdr" - _ = store.UpsertTransaction(context.Background(), payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.NewStatus) t.Run("fail_on_tx_build_and_sign", func(t *testing.T) { + _ = store.UpsertTransaction(context.Background(), payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.RPCTXStatus{OtherStatus: tss.NewStatus}) txServiceMock. On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). Return(nil, errors.New("signing failed")). Once() - _, err := BuildAndSubmitTransaction(context.Background(), "channel", payload, store, &txServiceMock) + _, err := txManager.BuildAndSubmitTransaction(context.Background(), "channel", payload) assert.Equal(t, "channel: Unable to sign/build transaction: signing failed", err.Error()) @@ -51,22 +59,24 @@ func TestBuildAndSubmitTransaction(t *testing.T) { }) t.Run("rpc_call_fail", func(t *testing.T) { - sendResp := tss.RPCSendTxResponse{} - sendResp.Code.OtherCodes = tss.RPCFailCode + _ = store.UpsertTransaction(context.Background(), payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.RPCTXStatus{OtherStatus: tss.NewStatus}) + sendResp := entities.RPCSendTransactionResult{Status: entities.ErrorStatus} + txServiceMock. On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). Return(feeBumpTx, nil). Once(). On("NetworkPassphrase"). Return(networkPass). - Once(). + Once() + rpcServiceMock. On("SendTransaction", feeBumpTxXDR). - Return(sendResp, errors.New("RPC Fail")). + Return(sendResp, errors.New("RPC down")). Once() - _, err := BuildAndSubmitTransaction(context.Background(), "channel", payload, store, &txServiceMock) + _, err := txManager.BuildAndSubmitTransaction(context.Background(), "channel", payload) - assert.Equal(t, "channel: RPC fail: RPC Fail", err.Error()) + assert.Equal(t, "channel: RPC fail: RPC fail: RPC down", err.Error()) var txStatus string err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) @@ -79,23 +89,63 @@ func TestBuildAndSubmitTransaction(t *testing.T) { assert.Equal(t, int(tss.RPCFailCode), tryStatus) }) - t.Run("rpc_resp_unmarshaling_error", func(t *testing.T) { - sendResp := tss.RPCSendTxResponse{} - sendResp.Code.OtherCodes = tss.UnMarshalBinaryCode + t.Run("rpc_resp_empty_errorresult_xdr", func(t *testing.T) { + _ = store.UpsertTransaction(context.Background(), payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.RPCTXStatus{OtherStatus: tss.NewStatus}) + sendResp := entities.RPCSendTransactionResult{ + Status: entities.PendingStatus, + ErrorResultXDR: "", + } + txServiceMock. On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). Return(feeBumpTx, nil). Once(). On("NetworkPassphrase"). Return(networkPass). + Once() + rpcServiceMock. + On("SendTransaction", feeBumpTxXDR). + Return(sendResp, nil). + Once() + + resp, err := txManager.BuildAndSubmitTransaction(context.Background(), "channel", payload) + + assert.Equal(t, entities.PendingStatus, resp.Status.RPCStatus) + assert.Equal(t, tss.EmptyCode, resp.Code.OtherCodes) + assert.Empty(t, err) + + var txStatus string + err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) + require.NoError(t, err) + assert.Equal(t, txStatus, string(entities.PendingStatus)) + + var tryStatus int + err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) + require.NoError(t, err) + assert.Equal(t, int(tss.EmptyCode), tryStatus) + }) + t.Run("rpc_resp_has_unparsable_errorresult_xdr", func(t *testing.T) { + _ = store.UpsertTransaction(context.Background(), payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.RPCTXStatus{OtherStatus: tss.NewStatus}) + sendResp := entities.RPCSendTransactionResult{ + Status: entities.ErrorStatus, + ErrorResultXDR: "ABCD", + } + + txServiceMock. + On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). + Return(feeBumpTx, nil). Once(). + On("NetworkPassphrase"). + Return(networkPass). + Once() + rpcServiceMock. On("SendTransaction", feeBumpTxXDR). - Return(sendResp, errors.New("unable to unmarshal")). + Return(sendResp, nil). Once() - _, err := BuildAndSubmitTransaction(context.Background(), "channel", payload, store, &txServiceMock) + _, err := txManager.BuildAndSubmitTransaction(context.Background(), "channel", payload) - assert.Equal(t, "channel: RPC fail: unable to unmarshal", err.Error()) + assert.Equal(t, "channel: RPC fail: parse error result xdr string: unable to unmarshal errorResultXDR: ABCD", err.Error()) var txStatus string err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) @@ -105,39 +155,41 @@ func TestBuildAndSubmitTransaction(t *testing.T) { var tryStatus int err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) require.NoError(t, err) - assert.Equal(t, int(tss.UnMarshalBinaryCode), tryStatus) + assert.Equal(t, int(tss.UnmarshalBinaryCode), tryStatus) }) t.Run("rpc_returns_response", func(t *testing.T) { - sendResp := tss.RPCSendTxResponse{} - sendResp.Status = tss.TryAgainLaterStatus - sendResp.TransactionHash = feeBumpTxHash - sendResp.TransactionXDR = feeBumpTxXDR - sendResp.Code.TxResultCode = xdr.TransactionResultCodeTxInsufficientFee + _ = store.UpsertTransaction(context.Background(), payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.RPCTXStatus{OtherStatus: tss.NewStatus}) + sendResp := entities.RPCSendTransactionResult{ + Status: entities.ErrorStatus, + ErrorResultXDR: "AAAAAAAAAMj////9AAAAAA==", + } + txServiceMock. On("SignAndBuildNewFeeBumpTransaction", context.Background(), payload.TransactionXDR). Return(feeBumpTx, nil). Once(). On("NetworkPassphrase"). Return(networkPass). - Once(). + Once() + rpcServiceMock. On("SendTransaction", feeBumpTxXDR). Return(sendResp, nil). Once() - resp, err := BuildAndSubmitTransaction(context.Background(), "channel", payload, store, &txServiceMock) + resp, err := txManager.BuildAndSubmitTransaction(context.Background(), "channel", payload) - assert.Equal(t, tss.TryAgainLaterStatus, resp.Status) - assert.Equal(t, xdr.TransactionResultCodeTxInsufficientFee, resp.Code.TxResultCode) + assert.Equal(t, entities.ErrorStatus, resp.Status.RPCStatus) + assert.Equal(t, xdr.TransactionResultCodeTxTooLate, resp.Code.TxResultCode) assert.Empty(t, err) var txStatus string err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) require.NoError(t, err) - assert.Equal(t, string(tss.TryAgainLaterStatus), txStatus) + assert.Equal(t, string(entities.ErrorStatus), txStatus) var tryStatus int err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) require.NoError(t, err) - assert.Equal(t, int(xdr.TransactionResultCodeTxInsufficientFee), tryStatus) + assert.Equal(t, int(xdr.TransactionResultCodeTxTooLate), tryStatus) }) } diff --git a/internal/tss/services/transaction_service.go b/internal/tss/services/transaction_service.go new file mode 100644 index 0000000..b65bae1 --- /dev/null +++ b/internal/tss/services/transaction_service.go @@ -0,0 +1,127 @@ +package services + +import ( + "context" + "fmt" + + "github.com/stellar/go/clients/horizonclient" + "github.com/stellar/go/txnbuild" + "github.com/stellar/wallet-backend/internal/signing" + tsserror "github.com/stellar/wallet-backend/internal/tss/errors" +) + +type TransactionService interface { + NetworkPassphrase() string + SignAndBuildNewFeeBumpTransaction(ctx context.Context, origTxXdr string) (*txnbuild.FeeBumpTransaction, error) +} + +type transactionService struct { + DistributionAccountSignatureClient signing.SignatureClient + ChannelAccountSignatureClient signing.SignatureClient + HorizonClient horizonclient.ClientInterface + BaseFee int64 +} + +var _ TransactionService = (*transactionService)(nil) + +type TransactionServiceOptions struct { + DistributionAccountSignatureClient signing.SignatureClient + ChannelAccountSignatureClient signing.SignatureClient + HorizonClient horizonclient.ClientInterface + BaseFee int64 +} + +func (o *TransactionServiceOptions) ValidateOptions() error { + if o.DistributionAccountSignatureClient == nil { + return fmt.Errorf("distribution account signature client cannot be nil") + } + + if o.ChannelAccountSignatureClient == nil { + return fmt.Errorf("channel account signature client cannot be nil") + } + + if o.HorizonClient == nil { + return fmt.Errorf("horizon client cannot be nil") + } + + if o.BaseFee < int64(txnbuild.MinBaseFee) { + return fmt.Errorf("base fee is lower than the minimum network fee") + } + + return nil +} + +func NewTransactionService(opts TransactionServiceOptions) (*transactionService, error) { + if err := opts.ValidateOptions(); err != nil { + return nil, err + } + return &transactionService{ + DistributionAccountSignatureClient: opts.DistributionAccountSignatureClient, + ChannelAccountSignatureClient: opts.ChannelAccountSignatureClient, + HorizonClient: opts.HorizonClient, + BaseFee: opts.BaseFee, + }, nil +} + +func (t *transactionService) NetworkPassphrase() string { + return t.DistributionAccountSignatureClient.NetworkPassphrase() +} + +func (t *transactionService) SignAndBuildNewFeeBumpTransaction(ctx context.Context, origTxXdr string) (*txnbuild.FeeBumpTransaction, error) { + genericTx, err := txnbuild.TransactionFromXDR(origTxXdr) + if err != nil { + return nil, tsserror.OriginalXDRMalformed + } + originalTx, txEmpty := genericTx.Transaction() + if !txEmpty { + return nil, tsserror.OriginalXDRMalformed + } + channelAccountPublicKey, err := t.ChannelAccountSignatureClient.GetAccountPublicKey(ctx) + if err != nil { + return nil, fmt.Errorf("getting channel account public key: %w", err) + } + channelAccount, err := t.HorizonClient.AccountDetail(horizonclient.AccountRequest{AccountID: channelAccountPublicKey}) + if err != nil { + return nil, fmt.Errorf("getting channel account details from horizon: %w", err) + } + tx, err := txnbuild.NewTransaction( + txnbuild.TransactionParams{ + SourceAccount: &channelAccount, + Operations: originalTx.Operations(), + BaseFee: int64(t.BaseFee), + Preconditions: txnbuild.Preconditions{ + TimeBounds: txnbuild.NewTimeout(300), + }, + IncrementSequenceNum: true, + }, + ) + if err != nil { + return nil, fmt.Errorf("building transaction: %w", err) + } + tx, err = t.ChannelAccountSignatureClient.SignStellarTransaction(ctx, tx, channelAccountPublicKey) + if err != nil { + return nil, fmt.Errorf("signing transaction with channel account: %w", err) + } + // Wrap the transaction in a fee bump tx, signed by the distribution account + distributionAccountPublicKey, err := t.DistributionAccountSignatureClient.GetAccountPublicKey(ctx) + if err != nil { + return nil, fmt.Errorf("getting distribution account public key: %w", err) + } + + feeBumpTx, err := txnbuild.NewFeeBumpTransaction( + txnbuild.FeeBumpTransactionParams{ + Inner: tx, + FeeAccount: distributionAccountPublicKey, + BaseFee: int64(t.BaseFee), + }, + ) + if err != nil { + return nil, fmt.Errorf("building fee-bump transaction %w", err) + } + + feeBumpTx, err = t.DistributionAccountSignatureClient.SignStellarFeeBumpTransaction(ctx, feeBumpTx) + if err != nil { + return nil, fmt.Errorf("signing the fee bump transaction with distribution account: %w", err) + } + return feeBumpTx, nil +} diff --git a/internal/tss/services/transaction_service_test.go b/internal/tss/services/transaction_service_test.go new file mode 100644 index 0000000..4dd137c --- /dev/null +++ b/internal/tss/services/transaction_service_test.go @@ -0,0 +1,236 @@ +package services + +import ( + "context" + "errors" + "testing" + + "github.com/stellar/go/clients/horizonclient" + "github.com/stellar/go/keypair" + "github.com/stellar/go/protocols/horizon" + "github.com/stellar/go/txnbuild" + "github.com/stellar/wallet-backend/internal/signing" + tsserror "github.com/stellar/wallet-backend/internal/tss/errors" + "github.com/stellar/wallet-backend/internal/tss/utils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" +) + +func TestValidateOptions(t *testing.T) { + t.Run("return_error_when_distribution_signature_client_nil", func(t *testing.T) { + opts := TransactionServiceOptions{ + DistributionAccountSignatureClient: nil, + ChannelAccountSignatureClient: &signing.SignatureClientMock{}, + HorizonClient: &horizonclient.MockClient{}, + BaseFee: 114, + } + err := opts.ValidateOptions() + assert.Equal(t, "distribution account signature client cannot be nil", err.Error()) + + }) + + t.Run("return_error_when_channel_signature_client_nil", func(t *testing.T) { + opts := TransactionServiceOptions{ + DistributionAccountSignatureClient: &signing.SignatureClientMock{}, + ChannelAccountSignatureClient: nil, + HorizonClient: &horizonclient.MockClient{}, + BaseFee: 114, + } + err := opts.ValidateOptions() + assert.Equal(t, "channel account signature client cannot be nil", err.Error()) + }) + + t.Run("return_error_when_horizon_client_nil", func(t *testing.T) { + opts := TransactionServiceOptions{ + DistributionAccountSignatureClient: &signing.SignatureClientMock{}, + ChannelAccountSignatureClient: &signing.SignatureClientMock{}, + HorizonClient: nil, + BaseFee: 114, + } + err := opts.ValidateOptions() + assert.Equal(t, "horizon client cannot be nil", err.Error()) + }) + + t.Run("return_error_when_base_fee_too_low", func(t *testing.T) { + opts := TransactionServiceOptions{ + DistributionAccountSignatureClient: &signing.SignatureClientMock{}, + ChannelAccountSignatureClient: &signing.SignatureClientMock{}, + HorizonClient: &horizonclient.MockClient{}, + BaseFee: txnbuild.MinBaseFee - 10, + } + err := opts.ValidateOptions() + assert.Equal(t, "base fee is lower than the minimum network fee", err.Error()) + }) +} + +func TestSignAndBuildNewFeeBumpTransaction(t *testing.T) { + distributionAccountSignatureClient := signing.SignatureClientMock{} + defer distributionAccountSignatureClient.AssertExpectations(t) + channelAccountSignatureClient := signing.SignatureClientMock{} + defer channelAccountSignatureClient.AssertExpectations(t) + horizonClient := horizonclient.MockClient{} + defer horizonClient.AssertExpectations(t) + txService, _ := NewTransactionService(TransactionServiceOptions{ + DistributionAccountSignatureClient: &distributionAccountSignatureClient, + ChannelAccountSignatureClient: &channelAccountSignatureClient, + HorizonClient: &horizonClient, + BaseFee: 114, + }) + + txStr, _ := utils.BuildTestTransaction().Base64() + + t.Run("malformed_transaction_string", func(t *testing.T) { + feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), "abcd") + assert.Empty(t, feeBumpTx) + assert.ErrorIs(t, tsserror.OriginalXDRMalformed, err) + }) + + t.Run("channel_account_signature_client_get_account_public_key_err", func(t *testing.T) { + channelAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return("", errors.New("channel accounts unavailable")). + Once() + + feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) + assert.Empty(t, feeBumpTx) + assert.Equal(t, "getting channel account public key: channel accounts unavailable", err.Error()) + }) + + t.Run("horizon_client_get_account_detail_err", func(t *testing.T) { + channelAccount := keypair.MustRandom() + channelAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return(channelAccount.Address(), nil). + Once() + + horizonClient. + On("AccountDetail", horizonclient.AccountRequest{ + AccountID: channelAccount.Address(), + }). + Return(horizon.Account{}, errors.New("horizon down")). + Once() + + feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) + assert.Empty(t, feeBumpTx) + assert.Equal(t, "getting channel account details from horizon: horizon down", err.Error()) + }) + + t.Run("horizon_client_sign_stellar_transaction_w_channel_account_err", func(t *testing.T) { + channelAccount := keypair.MustRandom() + channelAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return(channelAccount.Address(), nil). + Once(). + On("SignStellarTransaction", context.Background(), mock.AnythingOfType("*txnbuild.Transaction"), []string{channelAccount.Address()}). + Return(nil, errors.New("unable to sign")). + Once() + + horizonClient. + On("AccountDetail", horizonclient.AccountRequest{ + AccountID: channelAccount.Address(), + }). + Return(horizon.Account{AccountID: channelAccount.Address(), Sequence: 1}, nil). + Once() + + feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) + assert.Empty(t, feeBumpTx) + assert.Equal(t, "signing transaction with channel account: unable to sign", err.Error()) + }) + + t.Run("distribution_account_signature_client_get_account_public_key_err", func(t *testing.T) { + channelAccount := keypair.MustRandom() + signedTx := txnbuild.Transaction{} + channelAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return(channelAccount.Address(), nil). + Once(). + On("SignStellarTransaction", context.Background(), mock.AnythingOfType("*txnbuild.Transaction"), []string{channelAccount.Address()}). + Return(&signedTx, nil). + Once() + + distributionAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return("", errors.New("client down")). + Once() + + horizonClient. + On("AccountDetail", horizonclient.AccountRequest{ + AccountID: channelAccount.Address(), + }). + Return(horizon.Account{AccountID: channelAccount.Address(), Sequence: 1}, nil). + Once() + + feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) + assert.Empty(t, feeBumpTx) + assert.Equal(t, "getting distribution account public key: client down", err.Error()) + }) + + t.Run("horizon_client_sign_stellar_transaction_w_distribition_account_err", func(t *testing.T) { + account := keypair.MustRandom() + signedTx := utils.BuildTestTransaction() + channelAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return(account.Address(), nil). + Once(). + On("SignStellarTransaction", context.Background(), mock.AnythingOfType("*txnbuild.Transaction"), []string{account.Address()}). + Return(signedTx, nil). + Once() + + distributionAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return(account.Address(), nil). + Once(). + On("SignStellarFeeBumpTransaction", context.Background(), mock.AnythingOfType("*txnbuild.FeeBumpTransaction")). + Return(nil, errors.New("unable to sign")). + Once() + + horizonClient. + On("AccountDetail", horizonclient.AccountRequest{ + AccountID: account.Address(), + }). + Return(horizon.Account{AccountID: account.Address(), Sequence: 1}, nil). + Once() + + feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) + assert.Empty(t, feeBumpTx) + assert.Equal(t, "signing the fee bump transaction with distribution account: unable to sign", err.Error()) + }) + + t.Run("returns_signed_tx", func(t *testing.T) { + account := keypair.MustRandom() + signedTx := utils.BuildTestTransaction() + testFeeBumpTx, _ := txnbuild.NewFeeBumpTransaction( + txnbuild.FeeBumpTransactionParams{ + Inner: signedTx, + FeeAccount: account.Address(), + BaseFee: int64(100), + }, + ) + channelAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return(account.Address(), nil). + Once(). + On("SignStellarTransaction", context.Background(), mock.AnythingOfType("*txnbuild.Transaction"), []string{account.Address()}). + Return(signedTx, nil). + Once() + + distributionAccountSignatureClient. + On("GetAccountPublicKey", context.Background()). + Return(account.Address(), nil). + Once(). + On("SignStellarFeeBumpTransaction", context.Background(), mock.AnythingOfType("*txnbuild.FeeBumpTransaction")). + Return(testFeeBumpTx, nil). + Once() + + horizonClient. + On("AccountDetail", horizonclient.AccountRequest{ + AccountID: account.Address(), + }). + Return(horizon.Account{AccountID: account.Address(), Sequence: 1}, nil). + Once() + + feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) + assert.Equal(t, feeBumpTx, testFeeBumpTx) + assert.Empty(t, err) + }) +} diff --git a/internal/tss/services/types.go b/internal/tss/services/types.go deleted file mode 100644 index f395190..0000000 --- a/internal/tss/services/types.go +++ /dev/null @@ -1,7 +0,0 @@ -package services - -import "github.com/stellar/wallet-backend/internal/tss" - -type Service interface { - ProcessPayload(payload tss.Payload) -} diff --git a/internal/tss/services/webhook_handler_service.go b/internal/tss/services/webhook_handler_service.go deleted file mode 100644 index 33c837e..0000000 --- a/internal/tss/services/webhook_handler_service.go +++ /dev/null @@ -1,19 +0,0 @@ -package services - -import ( - "github.com/stellar/wallet-backend/internal/tss" -) - -type webhookHandlerService struct { - channel tss.Channel -} - -func NewWebhookHandlerService(channel tss.Channel) Service { - return &webhookHandlerService{ - channel: channel, - } -} - -func (p *webhookHandlerService) ProcessPayload(payload tss.Payload) { - // fill in later -} diff --git a/internal/tss/store/store.go b/internal/tss/store/store.go index 866f1cc..b7953b0 100644 --- a/internal/tss/store/store.go +++ b/internal/tss/store/store.go @@ -38,7 +38,7 @@ func (s *store) UpsertTransaction(ctx context.Context, webhookURL string, txHash current_status = $4, updated_at = NOW(); ` - _, err := s.DB.ExecContext(ctx, q, txHash, txXDR, webhookURL, string(status)) + _, err := s.DB.ExecContext(ctx, q, txHash, txXDR, webhookURL, status.Status()) if err != nil { return fmt.Errorf("inserting/updatig tss transaction: %w", err) } diff --git a/internal/tss/store/store_test.go b/internal/tss/store/store_test.go index 57709ab..2987a27 100644 --- a/internal/tss/store/store_test.go +++ b/internal/tss/store/store_test.go @@ -7,6 +7,7 @@ import ( "github.com/stellar/go/xdr" "github.com/stellar/wallet-backend/internal/db" "github.com/stellar/wallet-backend/internal/db/dbtest" + "github.com/stellar/wallet-backend/internal/entities" "github.com/stellar/wallet-backend/internal/tss" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -20,7 +21,7 @@ func TestUpsertTransaction(t *testing.T) { defer dbConnectionPool.Close() store := NewStore(dbConnectionPool) t.Run("insert", func(t *testing.T) { - _ = store.UpsertTransaction(context.Background(), "www.stellar.org", "hash", "xdr", tss.NewStatus) + _ = store.UpsertTransaction(context.Background(), "www.stellar.org", "hash", "xdr", tss.RPCTXStatus{OtherStatus: tss.NewStatus}) var status string err = dbConnectionPool.GetContext(context.Background(), &status, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, "hash") @@ -29,13 +30,13 @@ func TestUpsertTransaction(t *testing.T) { }) t.Run("update", func(t *testing.T) { - _ = store.UpsertTransaction(context.Background(), "www.stellar.org", "hash", "xdr", tss.NewStatus) - _ = store.UpsertTransaction(context.Background(), "www.stellar.org", "hash", "xdr", tss.SuccessStatus) + _ = store.UpsertTransaction(context.Background(), "www.stellar.org", "hash", "xdr", tss.RPCTXStatus{OtherStatus: tss.NewStatus}) + _ = store.UpsertTransaction(context.Background(), "www.stellar.org", "hash", "xdr", tss.RPCTXStatus{RPCStatus: entities.SuccessStatus}) var status string err = dbConnectionPool.GetContext(context.Background(), &status, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, "hash") require.NoError(t, err) - assert.Equal(t, status, string(tss.SuccessStatus)) + assert.Equal(t, status, string(entities.SuccessStatus)) var numRows int err = dbConnectionPool.GetContext(context.Background(), &numRows, `SELECT count(*) FROM tss_transactions WHERE transaction_hash = $1`, "hash") diff --git a/internal/tss/types.go b/internal/tss/types.go index a0aafcb..6836a70 100644 --- a/internal/tss/types.go +++ b/internal/tss/types.go @@ -1,19 +1,85 @@ package tss -import "github.com/stellar/go/xdr" +import ( + "bytes" + "encoding/base64" + "fmt" + "strconv" + + xdr3 "github.com/stellar/go-xdr/xdr3" + "github.com/stellar/go/xdr" + "github.com/stellar/wallet-backend/internal/entities" +) + +type RPCGetIngestTxResponse struct { + // A status that indicated whether this transaction failed or successly made it to the ledger + Status entities.RPCStatus + // The error code that is derived by deserialzing the ResultXdr string in the sendTransaction response + // list of possible errror codes: https://developers.stellar.org/docs/data/horizon/api-reference/errors/result-codes/transactions + Code RPCTXCode + // The raw TransactionEnvelope XDR for this transaction + EnvelopeXDR string + // The raw TransactionResult XDR of the envelopeXdr + ResultXDR string + // The unix timestamp of when the transaction was included in the ledger + CreatedAt int64 +} + +//nolint:unused +func ParseToRPCGetIngestTxResponse(result entities.RPCGetTransactionResult, err error) (RPCGetIngestTxResponse, error) { + if err != nil { + return RPCGetIngestTxResponse{Status: entities.ErrorStatus}, err + } + + getIngestTxResponse := RPCGetIngestTxResponse{ + Status: result.Status, + EnvelopeXDR: result.EnvelopeXDR, + ResultXDR: result.ResultXDR, + } + if getIngestTxResponse.Status != entities.NotFoundStatus { + getIngestTxResponse.CreatedAt, err = strconv.ParseInt(result.CreatedAt, 10, 64) + if err != nil { + return RPCGetIngestTxResponse{Status: entities.ErrorStatus}, fmt.Errorf("unable to parse createdAt: %w", err) + } + } + getIngestTxResponse.Code, err = parseSendTransactionErrorXDR(result.ResultXDR) + if err != nil { + return getIngestTxResponse, fmt.Errorf("parse error result xdr string: %w", err) + } + return getIngestTxResponse, nil +} + +type OtherStatus string -type RPCTXStatus string type OtherCodes int32 type TransactionResultCode int32 +const ( + NewStatus OtherStatus = "NEW" + NoStatus OtherStatus = "" +) + +type RPCTXStatus struct { + RPCStatus entities.RPCStatus + OtherStatus OtherStatus +} + +func (s RPCTXStatus) Status() string { + if s.OtherStatus != NoStatus { + return string(s.OtherStatus) + } + return string(s.RPCStatus) +} + const ( // Do not use NoCode NoCode OtherCodes = 0 // These values need to not overlap the values in xdr.TransactionResultCode NewCode OtherCodes = 100 RPCFailCode OtherCodes = 101 - UnMarshalBinaryCode OtherCodes = 102 + UnmarshalBinaryCode OtherCodes = 102 + EmptyCode OtherCodes = 103 ) type RPCTXCode struct { @@ -28,19 +94,14 @@ func (c RPCTXCode) Code() int { return int(c.TxResultCode) } -const ( - // Brand new transaction, not sent to RPC yet - NewStatus RPCTXStatus = "NEW" - // RPC sendTransaction statuses - PendingStatus RPCTXStatus = "PENDING" - DuplicateStatus RPCTXStatus = "DUPLICATE" - TryAgainLaterStatus RPCTXStatus = "TRY_AGAIN_LATER" - ErrorStatus RPCTXStatus = "ERROR" - // RPC getTransaction(s) statuses - NotFoundStatus RPCTXStatus = "NOT_FOUND" - FailedStatus RPCTXStatus = "FAILED" - SuccessStatus RPCTXStatus = "SUCCESS" -) +var FinalErrorCodes = []xdr.TransactionResultCode{ + xdr.TransactionResultCodeTxSuccess, + xdr.TransactionResultCodeTxFailed, + xdr.TransactionResultCodeTxMissingOperation, + xdr.TransactionResultCodeTxInsufficientBalance, + xdr.TransactionResultCodeTxBadAuthExtra, + xdr.TransactionResultCodeTxMalformed, +} var NonJitterErrorCodes = []xdr.TransactionResultCode{ xdr.TransactionResultCodeTxTooEarly, @@ -53,20 +114,6 @@ var JitterErrorCodes = []xdr.TransactionResultCode{ xdr.TransactionResultCodeTxInternalError, } -type RPCGetIngestTxResponse struct { - // A status that indicated whether this transaction failed or successly made it to the ledger - Status RPCTXStatus - // The error code that is derived by deserialzing the ResultXdr string in the sendTransaction response - // list of possible errror codes: https://developers.stellar.org/docs/data/horizon/api-reference/errors/result-codes/transactions - Code RPCTXCode - // The raw TransactionEnvelope XDR for this transaction - EnvelopeXDR string - // The raw TransactionResult XDR of the envelopeXdr - ResultXDR string - // The unix timestamp of when the transaction was included in the ledger - CreatedAt int64 -} - type RPCSendTxResponse struct { // The hash of the transaction submitted to RPC TransactionHash string @@ -78,29 +125,42 @@ type RPCSendTxResponse struct { Code RPCTXCode } -type Payload struct { - WebhookURL string - // The hash of the transaction xdr submitted by the client - the id of the transaction submitted by a client - TransactionHash string - // The xdr of the transaction - TransactionXDR string - // Relevant fields in an RPC sendTransaction response - RpcSubmitTxResponse RPCSendTxResponse - // Relevant fields in the transaction list inside the RPC getTransactions response - RpcGetIngestTxResponse RPCGetIngestTxResponse -} - -type RPCResult struct { - Status string `json:"status"` - EnvelopeXDR string `json:"envelopeXdr"` - ResultXDR string `json:"resultXdr"` - ErrorResultXDR string `json:"errorResultXdr"` - Hash string `json:"hash"` - CreatedAt string `json:"createdAt"` +func ParseToRPCSendTxResponse(transactionXDR string, result entities.RPCSendTransactionResult, err error) (RPCSendTxResponse, error) { + sendTxResponse := RPCSendTxResponse{} + sendTxResponse.TransactionXDR = transactionXDR + if err != nil { + sendTxResponse.Status.RPCStatus = entities.ErrorStatus + sendTxResponse.Code.OtherCodes = RPCFailCode + return sendTxResponse, fmt.Errorf("RPC fail: %w", err) + } + sendTxResponse.Status.RPCStatus = result.Status + sendTxResponse.TransactionHash = result.Hash + sendTxResponse.Code, err = parseSendTransactionErrorXDR(result.ErrorResultXDR) + if err != nil { + return sendTxResponse, fmt.Errorf("parse error result xdr string: %w", err) + } + return sendTxResponse, nil } -type RPCResponse struct { - RPCResult `json:"result"` +func parseSendTransactionErrorXDR(errorResultXDR string) (RPCTXCode, error) { + if errorResultXDR == "" { + return RPCTXCode{ + OtherCodes: EmptyCode, + }, nil + } + unmarshalErr := "unable to unmarshal errorResultXDR: %s" + decodedBytes, err := base64.StdEncoding.DecodeString(errorResultXDR) + if err != nil { + return RPCTXCode{OtherCodes: UnmarshalBinaryCode}, fmt.Errorf(unmarshalErr, errorResultXDR) + } + var errorResult xdr.TransactionResult + _, err = xdr3.Unmarshal(bytes.NewReader(decodedBytes), &errorResult) + if err != nil { + return RPCTXCode{OtherCodes: UnmarshalBinaryCode}, fmt.Errorf(unmarshalErr, errorResultXDR) + } + return RPCTXCode{ + TxResultCode: errorResult.Result.Code, + }, nil } type TSSResponse struct { @@ -112,6 +172,18 @@ type TSSResponse struct { ResultXDR string `json:"resultXdr"` } +type Payload struct { + WebhookURL string + // The hash of the transaction xdr submitted by the client - the id of the transaction submitted by a client + TransactionHash string + // The xdr of the transaction + TransactionXDR string + // Relevant fields in an RPC sendTransaction response + RpcSubmitTxResponse RPCSendTxResponse + // Relevant fields in the transaction list inside the RPC getTransactions response + RpcGetIngestTxResponse RPCGetIngestTxResponse +} + type Channel interface { Send(payload Payload) Receive(payload Payload) diff --git a/internal/tss/types_curr.go b/internal/tss/types_curr.go new file mode 100644 index 0000000..cd8f17e --- /dev/null +++ b/internal/tss/types_curr.go @@ -0,0 +1,148 @@ +package tss + +/* +import "github.com/stellar/go/xdr" + +type RPCTXStatus string +type OtherCodes int32 + +type TransactionResultCode int32 + +const ( + // Do not use NoCode + NoCode OtherCodes = 0 + // These values need to not overlap the values in xdr.TransactionResultCode + NewCode OtherCodes = 100 + RPCFailCode OtherCodes = 101 + UnMarshalBinaryCode OtherCodes = 102 +) + +type RPCTXCode struct { + TxResultCode xdr.TransactionResultCode + OtherCodes OtherCodes +} + +func (c RPCTXCode) Code() int { + if c.OtherCodes != NoCode { + return int(c.OtherCodes) + } + return int(c.TxResultCode) +} + +const ( + // Brand new transaction, not sent to RPC yet + NewStatus RPCTXStatus = "NEW" + // RPC sendTransaction statuses + PendingStatus RPCTXStatus = "PENDING" + DuplicateStatus RPCTXStatus = "DUPLICATE" + TryAgainLaterStatus RPCTXStatus = "TRY_AGAIN_LATER" + ErrorStatus RPCTXStatus = "ERROR" + // RPC getTransaction(s) statuses + NotFoundStatus RPCTXStatus = "NOT_FOUND" + FailedStatus RPCTXStatus = "FAILED" + SuccessStatus RPCTXStatus = "SUCCESS" +) + +var NonJitterErrorCodes = []xdr.TransactionResultCode{ + xdr.TransactionResultCodeTxTooEarly, + xdr.TransactionResultCodeTxTooLate, + xdr.TransactionResultCodeTxBadSeq, +} + +var JitterErrorCodes = []xdr.TransactionResultCode{ + xdr.TransactionResultCodeTxInsufficientFee, + xdr.TransactionResultCodeTxInternalError, +} + +type RPCGetIngestTxResponse struct { + // A status that indicated whether this transaction failed or successly made it to the ledger + Status RPCTXStatus + // The error code that is derived by deserialzing the ResultXdr string in the sendTransaction response + // list of possible errror codes: https://developers.stellar.org/docs/data/horizon/api-reference/errors/result-codes/transactions + Code RPCTXCode + // The raw TransactionEnvelope XDR for this transaction + EnvelopeXDR string + // The raw TransactionResult XDR of the envelopeXdr + ResultXDR string + Ledger int + ApplicationOrder int + // The unix timestamp of when the transaction was included in the ledger + CreatedAt int64 +} + +type RPCSendTxResponse struct { + // The hash of the transaction submitted to RPC + TransactionHash string + TransactionXDR string + // The status of an RPC sendTransaction call. Can be one of [PENDING, DUPLICATE, TRY_AGAIN_LATER, ERROR] + Status RPCTXStatus + // The (optional) error code that is derived by deserialzing the errorResultXdr string in the sendTransaction response + // list of possible errror codes: https://developers.stellar.org/docs/data/horizon/api-reference/errors/result-codes/transactions + Code RPCTXCode +} + +type Payload struct { + WebhookURL string + // The hash of the transaction xdr submitted by the client - the id of the transaction submitted by a client + TransactionHash string + // The xdr of the transaction + TransactionXDR string + // Relevant fields in an RPC sendTransaction response + RpcSubmitTxResponse RPCSendTxResponse + // Relevant fields in the transaction list inside the RPC getTransactions response + RpcGetIngestTxResponse RPCGetIngestTxResponse +} + +type Pagination struct { + Cursor string `json:"cursor,omitempty"` + Limit int `json:"limit"` +} + +type RPCParams struct { + Transaction string `json:"transaction,omitempty"` + Hash string `json:"hash,omitempty"` + StartLedger int `json:"startLedger,omitempty"` + Pagination Pagination `json:"pagination,omitempty"` +} + +type Transaction struct { + Status string `json:"status"` + ApplicationOrder int `json:"applicationOrder"` + FeeBump bool `json:"feeBump"` + EnvelopeXDR string `json:"envelopeXdr"` + ResultXDR string `json:"resultXdr"` + ResultMetaXDR string `json:"resultMetaXdr"` + Ledger int `json:"ledger"` + CreatedAt int `json:"createdAt"` +} + +type RPCResult struct { + Status string `json:"status"` + EnvelopeXDR string `json:"envelopeXdr"` + ResultXDR string `json:"resultXdr"` + ErrorResultXDR string `json:"errorResultXdr"` + Hash string `json:"hash"` + Transactions []Transaction `json:"transactions,omitempty"` + Cursor string `json:"cursor"` + CreatedAt string `json:"createdAt"` +} + +type RPCResponse struct { + RPCResult `json:"result"` +} + +type TSSResponse struct { + TransactionHash string `json:"tx_hash"` + TransactionResultCode string `json:"tx_result_code"` + Status string `json:"status"` + CreatedAt int64 `json:"created_at"` + EnvelopeXDR string `json:"envelopeXdr"` + ResultXDR string `json:"resultXdr"` +} + +type Channel interface { + Send(payload Payload) + Receive(payload Payload) + Stop() +} +*/ diff --git a/internal/tss/types_test.go b/internal/tss/types_test.go new file mode 100644 index 0000000..10b67d1 --- /dev/null +++ b/internal/tss/types_test.go @@ -0,0 +1,92 @@ +package tss + +import ( + "errors" + "testing" + + "github.com/stellar/go/xdr" + "github.com/stellar/wallet-backend/internal/entities" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestParseToRPCSendTxResponse(t *testing.T) { + t.Run("rpc_request_fails", func(t *testing.T) { + resp, err := ParseToRPCGetIngestTxResponse(entities.RPCGetTransactionResult{}, errors.New("sending sendTransaction request: sending POST request to RPC: connection failed")) + require.Error(t, err) + + assert.Equal(t, entities.ErrorStatus, resp.Status) + assert.Equal(t, "sending sendTransaction request: sending POST request to RPC: connection failed", err.Error()) + }) + + t.Run("response_has_empty_errorResultXdr", func(t *testing.T) { + resp, err := ParseToRPCSendTxResponse("", entities.RPCSendTransactionResult{ + Status: "PENDING", + ErrorResultXDR: "", + }, nil) + + assert.Equal(t, entities.PendingStatus, resp.Status.RPCStatus) + assert.Equal(t, EmptyCode, resp.Code.OtherCodes) + assert.Empty(t, err) + }) + + t.Run("response_has_unparsable_errorResultXdr", func(t *testing.T) { + resp, err := ParseToRPCSendTxResponse("", entities.RPCSendTransactionResult{ + ErrorResultXDR: "ABC123", + }, nil) + + assert.Equal(t, UnmarshalBinaryCode, resp.Code.OtherCodes) + assert.Equal(t, "parse error result xdr string: unable to unmarshal errorResultXDR: ABC123", err.Error()) + }) + + t.Run("response_has_errorResultXdr", func(t *testing.T) { + resp, err := ParseToRPCSendTxResponse("", entities.RPCSendTransactionResult{ + ErrorResultXDR: "AAAAAAAAAMj////9AAAAAA==", + }, nil) + + assert.Equal(t, xdr.TransactionResultCodeTxTooLate, resp.Code.TxResultCode) + assert.Empty(t, err) + }) +} + +func TestParseToRPCGetIngestTxResponse(t *testing.T) { + t.Run("rpc_request_fails", func(t *testing.T) { + resp, err := ParseToRPCGetIngestTxResponse(entities.RPCGetTransactionResult{}, errors.New("sending getTransaction request: sending POST request to RPC: connection failed")) + require.Error(t, err) + + assert.Equal(t, entities.ErrorStatus, resp.Status) + assert.Equal(t, "sending getTransaction request: sending POST request to RPC: connection failed", err.Error()) + }) + + t.Run("unable_to_parse_createdAt", func(t *testing.T) { + resp, err := ParseToRPCGetIngestTxResponse(entities.RPCGetTransactionResult{ + Status: "SUCCESS", + CreatedAt: "ABCD", + }, nil) + require.Error(t, err) + + assert.Equal(t, entities.ErrorStatus, resp.Status) + assert.Equal(t, "unable to parse createdAt: strconv.ParseInt: parsing \"ABCD\": invalid syntax", err.Error()) + }) + + t.Run("response_has_createdAt_field", func(t *testing.T) { + resp, err := ParseToRPCGetIngestTxResponse(entities.RPCGetTransactionResult{ + CreatedAt: "1234567", + }, nil) + require.NoError(t, err) + + assert.Equal(t, int64(1234567), resp.CreatedAt) + assert.Empty(t, err) + }) + + t.Run("response_has_errorResultXdr", func(t *testing.T) { + resp, err := ParseToRPCGetIngestTxResponse(entities.RPCGetTransactionResult{ + Status: entities.ErrorStatus, + CreatedAt: "1234567", + ResultXDR: "AAAAAAAAAMj////9AAAAAA==", + }, nil) + + assert.Equal(t, xdr.TransactionResultCodeTxTooLate, resp.Code.TxResultCode) + assert.Empty(t, err) + }) +} diff --git a/internal/tss/utils/helpers.go b/internal/tss/utils/helpers.go index 81cb448..dec166c 100644 --- a/internal/tss/utils/helpers.go +++ b/internal/tss/utils/helpers.go @@ -9,8 +9,8 @@ import ( func PayloadTOTSSResponse(payload tss.Payload) tss.TSSResponse { response := tss.TSSResponse{} response.TransactionHash = payload.TransactionHash - if payload.RpcSubmitTxResponse.Status != "" { - response.Status = string(payload.RpcSubmitTxResponse.Status) + if payload.RpcSubmitTxResponse.Status.Status() != "" { + response.Status = string(payload.RpcSubmitTxResponse.Status.Status()) response.TransactionResultCode = payload.RpcSubmitTxResponse.Code.TxResultCode.String() response.EnvelopeXDR = payload.RpcSubmitTxResponse.TransactionXDR } else if payload.RpcGetIngestTxResponse.Status != "" { diff --git a/internal/tss/utils/mocks.go b/internal/tss/utils/mocks.go deleted file mode 100644 index 597a6c9..0000000 --- a/internal/tss/utils/mocks.go +++ /dev/null @@ -1,50 +0,0 @@ -package utils - -import ( - "context" - "io" - "net/http" - - "github.com/stellar/go/txnbuild" - "github.com/stellar/wallet-backend/internal/tss" - "github.com/stretchr/testify/mock" -) - -type MockHTTPClient struct { - mock.Mock -} - -func (s *MockHTTPClient) Post(url, contentType string, body io.Reader) (resp *http.Response, err error) { - args := s.Called(url, contentType, body) - return args.Get(0).(*http.Response), args.Error(1) -} - -type TransactionServiceMock struct { - mock.Mock -} - -var _ TransactionService = (*TransactionServiceMock)(nil) - -func (t *TransactionServiceMock) NetworkPassphrase() string { - args := t.Called() - return args.String(0) -} - -func (t *TransactionServiceMock) SignAndBuildNewFeeBumpTransaction(ctx context.Context, origTxXdr string) (*txnbuild.FeeBumpTransaction, error) { - args := t.Called(ctx, origTxXdr) - if result := args.Get(0); result != nil { - return result.(*txnbuild.FeeBumpTransaction), args.Error(1) - } - return nil, args.Error(1) - -} - -func (t *TransactionServiceMock) SendTransaction(transactionXdr string) (tss.RPCSendTxResponse, error) { - args := t.Called(transactionXdr) - return args.Get(0).(tss.RPCSendTxResponse), args.Error(1) -} - -func (t *TransactionServiceMock) GetTransaction(transactionHash string) (tss.RPCGetIngestTxResponse, error) { - args := t.Called(transactionHash) - return args.Get(0).(tss.RPCGetIngestTxResponse), args.Error(1) -} diff --git a/internal/tss/utils/transaction_service.go b/internal/tss/utils/transaction_service.go index 129c1f0..9ffc5a4 100644 --- a/internal/tss/utils/transaction_service.go +++ b/internal/tss/utils/transaction_service.go @@ -1,5 +1,7 @@ package utils +/* + import ( "bytes" "context" @@ -23,11 +25,15 @@ type HTTPClient interface { Post(url string, t string, body io.Reader) (resp *http.Response, err error) } +var PageLimit = 200 + type TransactionService interface { NetworkPassphrase() string + UnmarshalTransactionResultXDR(transactionResultXDR string) (xdr.TransactionResult, error) SignAndBuildNewFeeBumpTransaction(ctx context.Context, origTxXdr string) (*txnbuild.FeeBumpTransaction, error) SendTransaction(transactionXdr string) (tss.RPCSendTxResponse, error) GetTransaction(transactionHash string) (tss.RPCGetIngestTxResponse, error) + GetTransactions(startLedger int, startCursor string, limit int) ([]tss.RPCGetIngestTxResponse, string, error) } type transactionService struct { @@ -155,23 +161,31 @@ func (t *transactionService) SignAndBuildNewFeeBumpTransaction(ctx context.Conte return feeBumpTx, nil } -func (t *transactionService) parseErrorResultXDR(errorResultXdr string) (tss.RPCTXCode, error) { +func (t *transactionService) UnmarshalTransactionResultXDR(transactionResultXDR string) (xdr.TransactionResult, error) { unMarshalErr := "unable to unmarshal errorResultXdr: %s" - decodedBytes, err := base64.StdEncoding.DecodeString(errorResultXdr) + decodedBytes, err := base64.StdEncoding.DecodeString(transactionResultXDR) if err != nil { - return tss.RPCTXCode{OtherCodes: tss.UnMarshalBinaryCode}, fmt.Errorf(unMarshalErr, errorResultXdr) + return xdr.TransactionResult{}, fmt.Errorf(unMarshalErr, transactionResultXDR) } - var errorResult xdr.TransactionResult - _, err = xdr3.Unmarshal(bytes.NewReader(decodedBytes), &errorResult) + var txResult xdr.TransactionResult + _, err = xdr3.Unmarshal(bytes.NewReader(decodedBytes), &txResult) if err != nil { - return tss.RPCTXCode{OtherCodes: tss.UnMarshalBinaryCode}, fmt.Errorf(unMarshalErr, errorResultXdr) + return xdr.TransactionResult{}, fmt.Errorf(unMarshalErr, transactionResultXDR) + } + return txResult, nil +} + +func (t *transactionService) parseErrorResultXDR(txResultXdr string) (tss.RPCTXCode, error) { + txResult, err := t.UnmarshalTransactionResultXDR(txResultXdr) + if err != nil { + return tss.RPCTXCode{OtherCodes: tss.UnMarshalBinaryCode}, fmt.Errorf("parse error result xdr: %s", err.Error()) } return tss.RPCTXCode{ - TxResultCode: errorResult.Result.Code, + TxResultCode: txResult.Result.Code, }, nil } -func (t *transactionService) sendRPCRequest(method string, params map[string]string) (tss.RPCResponse, error) { +func (t *transactionService) sendRPCRequest(method string, params tss.RPCParams) (tss.RPCResponse, error) { payload := map[string]interface{}{ "jsonrpc": "2.0", "id": 1, @@ -203,7 +217,7 @@ func (t *transactionService) sendRPCRequest(method string, params map[string]str } func (t *transactionService) SendTransaction(transactionXdr string) (tss.RPCSendTxResponse, error) { - rpcResponse, err := t.sendRPCRequest("sendTransaction", map[string]string{"transaction": transactionXdr}) + rpcResponse, err := t.sendRPCRequest("sendTransaction", tss.RPCParams{Transaction: transactionXdr}) sendTxResponse := tss.RPCSendTxResponse{} sendTxResponse.TransactionXDR = transactionXdr if err != nil { @@ -218,7 +232,7 @@ func (t *transactionService) SendTransaction(transactionXdr string) (tss.RPCSend } func (t *transactionService) GetTransaction(transactionHash string) (tss.RPCGetIngestTxResponse, error) { - rpcResponse, err := t.sendRPCRequest("getTransaction", map[string]string{"hash": transactionHash}) + rpcResponse, err := t.sendRPCRequest("getTransaction", tss.RPCParams{Hash: transactionHash}) if err != nil { return tss.RPCGetIngestTxResponse{Status: tss.ErrorStatus}, fmt.Errorf("RPC Fail: %s", err.Error()) } @@ -235,3 +249,41 @@ func (t *transactionService) GetTransaction(transactionHash string) (tss.RPCGetI getIngestTxResponse.Code, err = t.parseErrorResultXDR(rpcResponse.RPCResult.ResultXDR) return getIngestTxResponse, err } + +func (t *transactionService) GetTransactions(startLedger int, startCursor string, limit int) ([]tss.RPCGetIngestTxResponse, string, error) { + if limit > PageLimit { + return []tss.RPCGetIngestTxResponse{}, "", fmt.Errorf("limit cannot exceed") + } + params := tss.RPCParams{} + if startCursor != "" { + pagination := tss.Pagination{Cursor: startCursor, Limit: limit} + params.Pagination = pagination + } else { + pagination := tss.Pagination{Limit: limit} + params.Pagination = pagination + params.StartLedger = startLedger + } + rpcResponse, err := t.sendRPCRequest("getTransactions", params) + if err != nil { + return []tss.RPCGetIngestTxResponse{}, "", fmt.Errorf("RPC Fail: %s", err.Error()) + } + + var transactions []tss.RPCGetIngestTxResponse + + for _, tx := range rpcResponse.RPCResult.Transactions { + getIngestTxResponse := tss.RPCGetIngestTxResponse{} + getIngestTxResponse.Code, err = t.parseErrorResultXDR(tx.ResultXDR) + if err != nil { + return []tss.RPCGetIngestTxResponse{}, "", fmt.Errorf("unable to parse resultXdr: %s", err.Error()) + } + getIngestTxResponse.Status = tss.RPCTXStatus(tx.Status) + getIngestTxResponse.EnvelopeXDR = tx.EnvelopeXDR + getIngestTxResponse.ResultXDR = tx.ResultXDR + getIngestTxResponse.CreatedAt = int64(tx.CreatedAt) + getIngestTxResponse.Ledger = tx.Ledger + getIngestTxResponse.ApplicationOrder = tx.ApplicationOrder + transactions = append(transactions, getIngestTxResponse) + } + return transactions, rpcResponse.RPCResult.Cursor, nil +} +*/ diff --git a/internal/tss/utils/transaction_service_test.go b/internal/tss/utils/transaction_service_test.go index 3ef6372..93eee35 100644 --- a/internal/tss/utils/transaction_service_test.go +++ b/internal/tss/utils/transaction_service_test.go @@ -1,5 +1,7 @@ package utils +/* + import ( "bytes" "context" @@ -23,86 +25,6 @@ import ( "github.com/stretchr/testify/mock" ) -func TestValidateOptions(t *testing.T) { - t.Run("return_error_when_distribution_signature_client_nil", func(t *testing.T) { - opts := TransactionServiceOptions{ - DistributionAccountSignatureClient: nil, - ChannelAccountSignatureClient: &signing.SignatureClientMock{}, - HorizonClient: &horizonclient.MockClient{}, - RPCURL: "http://localhost:8000/soroban/rpc", - BaseFee: 114, - HTTPClient: &MockHTTPClient{}, - } - err := opts.ValidateOptions() - assert.Equal(t, "distribution account signature client cannot be nil", err.Error()) - - }) - - t.Run("return_error_when_channel_signature_client_nil", func(t *testing.T) { - opts := TransactionServiceOptions{ - DistributionAccountSignatureClient: &signing.SignatureClientMock{}, - ChannelAccountSignatureClient: nil, - HorizonClient: &horizonclient.MockClient{}, - RPCURL: "http://localhost:8000/soroban/rpc", - BaseFee: 114, - HTTPClient: &MockHTTPClient{}, - } - err := opts.ValidateOptions() - assert.Equal(t, "channel account signature client cannot be nil", err.Error()) - }) - - t.Run("return_error_when_horizon_client_nil", func(t *testing.T) { - opts := TransactionServiceOptions{ - DistributionAccountSignatureClient: &signing.SignatureClientMock{}, - ChannelAccountSignatureClient: &signing.SignatureClientMock{}, - HorizonClient: nil, - RPCURL: "http://localhost:8000/soroban/rpc", - BaseFee: 114, - HTTPClient: &MockHTTPClient{}, - } - err := opts.ValidateOptions() - assert.Equal(t, "horizon client cannot be nil", err.Error()) - }) - - t.Run("return_error_when_rpc_url_empty", func(t *testing.T) { - opts := TransactionServiceOptions{ - DistributionAccountSignatureClient: &signing.SignatureClientMock{}, - ChannelAccountSignatureClient: &signing.SignatureClientMock{}, - HorizonClient: &horizonclient.MockClient{}, - RPCURL: "", - BaseFee: 114, - HTTPClient: &MockHTTPClient{}, - } - err := opts.ValidateOptions() - assert.Equal(t, "rpc url cannot be empty", err.Error()) - }) - - t.Run("return_error_when_base_fee_too_low", func(t *testing.T) { - opts := TransactionServiceOptions{ - DistributionAccountSignatureClient: &signing.SignatureClientMock{}, - ChannelAccountSignatureClient: &signing.SignatureClientMock{}, - HorizonClient: &horizonclient.MockClient{}, - RPCURL: "http://localhost:8000/soroban/rpc", - BaseFee: txnbuild.MinBaseFee - 10, - HTTPClient: &MockHTTPClient{}, - } - err := opts.ValidateOptions() - assert.Equal(t, "base fee is lower than the minimum network fee", err.Error()) - }) - - t.Run("return_error_http_client_nil", func(t *testing.T) { - opts := TransactionServiceOptions{ - DistributionAccountSignatureClient: &signing.SignatureClientMock{}, - ChannelAccountSignatureClient: &signing.SignatureClientMock{}, - HorizonClient: &horizonclient.MockClient{}, - RPCURL: "http://localhost:8000/soroban/rpc", - BaseFee: 114, - } - err := opts.ValidateOptions() - assert.Equal(t, "http client cannot be nil", err.Error()) - }) -} - func TestSignAndBuildNewFeeBumpTransaction(t *testing.T) { distributionAccountSignatureClient := signing.SignatureClientMock{} defer distributionAccountSignatureClient.AssertExpectations(t) @@ -332,7 +254,7 @@ func TestSendRPCRequest(t *testing.T) { HTTPClient: &mockHTTPClient, }) method := "sendTransaction" - params := map[string]string{"transaction": "ABCD"} + params := tss.RPCParams{Transaction: "ABCD"} payload := map[string]interface{}{ "jsonrpc": "2.0", "id": 1, @@ -507,7 +429,7 @@ func TestSendTransaction(t *testing.T) { HTTPClient: &mockHTTPClient, }) method := "sendTransaction" - params := map[string]string{"transaction": "ABCD"} + params := tss.RPCParams{Transaction: "ABCD"} payload := map[string]interface{}{ "jsonrpc": "2.0", "id": 1, @@ -591,7 +513,7 @@ func TestGetTransaction(t *testing.T) { HTTPClient: &mockHTTPClient, }) method := "getTransaction" - params := map[string]string{"hash": "XYZ"} + params := tss.RPCParams{Hash: "XYZ"} payload := map[string]interface{}{ "jsonrpc": "2.0", "id": 1, @@ -646,3 +568,4 @@ func TestGetTransaction(t *testing.T) { }) } +*/ diff --git a/internal/utils/http_client.go b/internal/utils/http_client.go new file mode 100644 index 0000000..514abf0 --- /dev/null +++ b/internal/utils/http_client.go @@ -0,0 +1,21 @@ +package utils + +import ( + "io" + "net/http" + + "github.com/stretchr/testify/mock" +) + +type HTTPClient interface { + Post(url string, t string, body io.Reader) (resp *http.Response, err error) +} + +type MockHTTPClient struct { + mock.Mock +} + +func (s *MockHTTPClient) Post(url, contentType string, body io.Reader) (resp *http.Response, err error) { + args := s.Called(url, contentType, body) + return args.Get(0).(*http.Response), args.Error(1) +} diff --git a/internal/utils/ingestion_utils.go b/internal/utils/ingestion_utils.go index 80095ce..5a3fe20 100644 --- a/internal/utils/ingestion_utils.go +++ b/internal/utils/ingestion_utils.go @@ -12,6 +12,12 @@ func OperationID(ledgerNumber, txNumber, opNumber int32) string { return toid.New(ledgerNumber, txNumber, opNumber).String() } +func OperationResultRPC(txResult xdr.TransactionResult, opNumber int) *xdr.OperationResultTr { + results, _ := txResult.OperationResults() + tr := results[opNumber-1].MustTr() + return &tr +} + func OperationResult(tx ingest.LedgerTransaction, opNumber int) *xdr.OperationResultTr { results, _ := tx.Result.OperationResults() tr := results[opNumber-1].MustTr() @@ -62,6 +68,15 @@ func Memo(memo xdr.Memo, txHash string) (*string, string) { return nil, memoType.String() } +func SourceAccountRPC(op xdr.Operation, txEnvelope xdr.TransactionEnvelope) string { + account := op.SourceAccount + if account != nil { + return account.ToAccountId().Address() + } + + return txEnvelope.SourceAccount().ToAccountId().Address() +} + func SourceAccount(op xdr.Operation, tx ingest.LedgerTransaction) string { account := op.SourceAccount if account != nil { From f139f7a883b5ebde6c506852b98de3f4ee06c4a8 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Sun, 29 Sep 2024 20:07:01 -0700 Subject: [PATCH 079/113] rpc ingestor --- cmd/ingest.go | 48 +-- cmd/utils/custom_set_value_test.go | 2 + cmd/utils/global_options.go | 24 ++ internal/entities/rpc.go | 11 +- .../ingest/config/stellar-core_pubnet.cfg | 25 -- .../ingest/config/stellar-core_testnet.cfg | 25 -- internal/ingest/ingest.go | 151 ++----- internal/ingest/ingest_test.go | 61 --- internal/services/ingest.go | 370 +++++++----------- internal/services/ingest_test.go | 239 +++++------ .../rpc_service_mocks.go => mocks.go} | 10 +- internal/services/rpc_service.go | 12 +- internal/services/rpc_service_test.go | 92 +++-- internal/tss/router/router.go | 2 + internal/tss/router/router_test.go | 19 +- .../tss/services/transaction_manager_test.go | 4 +- internal/tss/store/store.go | 64 +++ internal/tss/store/store_test.go | 66 ++++ internal/tss/types.go | 31 +- internal/tss/types_test.go | 2 +- internal/tss/utils/helpers.go | 1 + 21 files changed, 583 insertions(+), 676 deletions(-) delete mode 100644 internal/ingest/config/stellar-core_pubnet.cfg delete mode 100644 internal/ingest/config/stellar-core_testnet.cfg delete mode 100644 internal/ingest/ingest_test.go rename internal/services/{servicesmocks/rpc_service_mocks.go => mocks.go} (66%) diff --git a/cmd/ingest.go b/cmd/ingest.go index 2e9c60b..6c47706 100644 --- a/cmd/ingest.go +++ b/cmd/ingest.go @@ -3,6 +3,8 @@ package cmd import ( "fmt" "go/types" + "net/http" + "time" _ "github.com/lib/pq" "github.com/spf13/cobra" @@ -11,6 +13,7 @@ import ( "github.com/stellar/wallet-backend/cmd/utils" "github.com/stellar/wallet-backend/internal/apptracker/sentry" "github.com/stellar/wallet-backend/internal/ingest" + tsschannels "github.com/stellar/wallet-backend/internal/tss/channels" ) type ingestCmd struct{} @@ -22,27 +25,15 @@ func (c *ingestCmd) Command() *cobra.Command { cfgOpts := config.ConfigOptions{ utils.DatabaseURLOption(&cfg.DatabaseURL), utils.LogLevelOption(&cfg.LogLevel), - utils.NetworkPassphraseOption(&cfg.NetworkPassphrase), utils.SentryDSNOption(&sentryDSN), utils.StellarEnvironmentOption(&stellarEnvironment), - { - Name: "captive-core-bin-path", - Usage: "Path to Captive Core's binary file.", - OptType: types.String, - CustomSetValue: utils.SetConfigOptionCaptiveCoreBinPath, - ConfigKey: &cfg.CaptiveCoreBinPath, - FlagDefault: "/usr/local/bin/stellar-core", - Required: true, - }, - { - Name: "captive-core-config-dir", - Usage: "Path to Captive Core's configuration files directory.", - OptType: types.String, - CustomSetValue: utils.SetConfigOptionCaptiveCoreConfigDir, - ConfigKey: &cfg.CaptiveCoreConfigDir, - FlagDefault: "./internal/ingest/config", - Required: true, - }, + utils.RPCURLOption(&cfg.RPCURL), + utils.StartLedgerOption(&cfg.StartLedger), + utils.EndLedgerOption(&cfg.EndLedger), + utils.WebhookHandlerServiceChannelMaxBufferSizeOption(&cfg.WebhookChannelMaxBufferSize), + utils.WebhookHandlerServiceChannelMaxWorkersOptions(&cfg.WebhookChannelMaxWorkers), + utils.WebhookHandlerServiceChannelMaxRetriesOption(&cfg.WebhookChannelMaxRetries), + utils.WebhookHandlerServiceChannelMinWaitBtwnRetriesMSOption(&cfg.WebhookChannelWaitBtwnTriesMS), { Name: "ledger-cursor-name", Usage: "Name of last synced ledger cursor, used to keep track of the last ledger ingested by the service. When starting up, ingestion will resume from the ledger number stored in this record. It should be an unique name per container as different containers would overwrite the cursor value of its peers when using the same cursor name.", @@ -59,20 +50,13 @@ func (c *ingestCmd) Command() *cobra.Command { FlagDefault: 0, Required: false, }, - { - Name: "end", - Usage: "Ledger number up to which ingestion should run. When not present, ingestion run indefinitely (live ingestion requires it to be empty).", - OptType: types.Int, - ConfigKey: &cfg.EndLedger, - FlagDefault: 0, - Required: false, - }, } cmd := &cobra.Command{ Use: "ingest", Short: "Run Ingestion service", PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + // SET UP WEBHOOK CHANNEL HERE if err := cfgOpts.RequireE(); err != nil { return fmt.Errorf("requiring values of config options: %w", err) } @@ -84,11 +68,21 @@ func (c *ingestCmd) Command() *cobra.Command { return fmt.Errorf("initializing app tracker: %w", err) } cfg.AppTracker = appTracker + cfg.WebhookChannel = tsschannels.NewWebhookChannel(tsschannels.WebhookChannelConfigs{ + HTTPClient: &http.Client{Timeout: 30 * time.Second}, + MaxBufferSize: cfg.WebhookChannelMaxBufferSize, + MaxWorkers: cfg.WebhookChannelMaxWorkers, + MaxRetries: cfg.WebhookChannelMaxRetries, + MinWaitBtwnRetriesMS: cfg.WebhookChannelWaitBtwnTriesMS, + }) return nil }, RunE: func(_ *cobra.Command, _ []string) error { return c.Run(cfg) }, + PersistentPostRun: func(_ *cobra.Command, _ []string) { + cfg.WebhookChannel.Stop() + }, } if err := cfgOpts.Init(cmd); err != nil { diff --git a/cmd/utils/custom_set_value_test.go b/cmd/utils/custom_set_value_test.go index c936900..dfe7487 100644 --- a/cmd/utils/custom_set_value_test.go +++ b/cmd/utils/custom_set_value_test.go @@ -220,6 +220,7 @@ func Test_SetConfigOptionLogLevel(t *testing.T) { } } +/* func TestSetConfigOptionCaptiveCoreBinPath(t *testing.T) { opts := struct{ binPath string }{} @@ -310,6 +311,7 @@ func TestSetConfigOptionCaptiveCoreConfigDir(t *testing.T) { }) } } +*/ func TestSetConfigOptionAssets(t *testing.T) { opts := struct{ assets []entities.Asset }{} diff --git a/cmd/utils/global_options.go b/cmd/utils/global_options.go index a705edf..a879903 100644 --- a/cmd/utils/global_options.go +++ b/cmd/utils/global_options.go @@ -297,6 +297,30 @@ func WebhookHandlerServiceChannelMinWaitBtwnRetriesMSOption(configKey *int) *con } } +func StartLedgerOption(configKey *int) *config.ConfigOption { + return &config.ConfigOption{ + Name: "start-ledger", + Usage: "ledger number to start getting transactions from", + OptType: types.Int, + ConfigKey: configKey, + FlagDefault: 0, + Required: true, + } + +} + +func EndLedgerOption(configKey *int) *config.ConfigOption { + return &config.ConfigOption{ + Name: "end-ledger", + Usage: "ledger number to end on", + OptType: types.Int, + ConfigKey: configKey, + FlagDefault: 0, + Required: true, + } + +} + func AWSOptions(awsRegionConfigKey *string, kmsKeyARN *string, required bool) config.ConfigOptions { awsOpts := config.ConfigOptions{ { diff --git a/internal/entities/rpc.go b/internal/entities/rpc.go index 9e83c08..aedad95 100644 --- a/internal/entities/rpc.go +++ b/internal/entities/rpc.go @@ -51,22 +51,23 @@ type RPCGetTransactionResult struct { type Transaction struct { Status RPCStatus `json:"status"` + Hash string `json:"hash"` ApplicationOrder int64 `json:"applicationOrder"` FeeBump bool `json:"feeBump"` EnvelopeXDR string `json:"envelopeXdr"` ResultXDR string `json:"resultXdr"` ResultMetaXDR string `json:"resultMetaXdr"` + Ledger int64 `json:"ledger"` DiagnosticEventsXDR string `json:"diagnosticEventsXdr"` - CreatedAt string `json:"createdAt"` - ErrorResultXDR string `json:"errorResultXdr"` + CreatedAt int64 `json:"createdAt"` } type RPCGetTransactionsResult struct { Transactions []Transaction `json:"transactions"` LatestLedger int64 `json:"latestLedger"` - LatestLedgerCloseTime string `json:"latestLedgerCloseTimestamp"` + LatestLedgerCloseTime int64 `json:"latestLedgerCloseTimestamp"` OldestLedger int64 `json:"oldestLedger"` - OldestLedgerCloseTime string `json:"oldestLedgerCloseTimestamp"` + OldestLedgerCloseTime int64 `json:"oldestLedgerCloseTimestamp"` Cursor string `json:"cursor"` } @@ -86,6 +87,6 @@ type RPCPagination struct { type RPCParams struct { Transaction string `json:"transaction,omitempty"` Hash string `json:"hash,omitempty"` - StartLedger int `json:"startLedger,omitempty"` + StartLedger int64 `json:"startLedger,omitempty"` Pagination RPCPagination `json:"pagination,omitempty"` } diff --git a/internal/ingest/config/stellar-core_pubnet.cfg b/internal/ingest/config/stellar-core_pubnet.cfg deleted file mode 100644 index 8394e87..0000000 --- a/internal/ingest/config/stellar-core_pubnet.cfg +++ /dev/null @@ -1,25 +0,0 @@ -# Stellar Pubnet validators -[[HOME_DOMAINS]] -HOME_DOMAIN="www.stellar.org" -QUALITY="HIGH" - -[[VALIDATORS]] -NAME="SDF 1" -PUBLIC_KEY="GCGB2S2KGYARPVIA37HYZXVRM2YZUEXA6S33ZU5BUDC6THSB62LZSTYH" -ADDRESS="core-live-a.stellar.org:11625" -HISTORY="curl -sf http://history.stellar.org/prd/core-live/core_live_001/{0} -o {1}" -HOME_DOMAIN="www.stellar.org" - -[[VALIDATORS]] -NAME="SDF 2" -PUBLIC_KEY="GCM6QMP3DLRPTAZW2UZPCPX2LF3SXWXKPMP3GKFZBDSF3QZGV2G5QSTK" -ADDRESS="core-live-b.stellar.org:11625" -HISTORY="curl -sf http://history.stellar.org/prd/core-live/core_live_002/{0} -o {1}" -HOME_DOMAIN="www.stellar.org" - -[[VALIDATORS]] -NAME="SDF 3" -PUBLIC_KEY="GABMKJM6I25XI4K7U6XWMULOUQIQ27BCTMLS6BYYSOWKTBUXVRJSXHYQ" -ADDRESS="core-live-c.stellar.org:11625" -HISTORY="curl -sf http://history.stellar.org/prd/core-live/core_live_003/{0} -o {1}" -HOME_DOMAIN="www.stellar.org" \ No newline at end of file diff --git a/internal/ingest/config/stellar-core_testnet.cfg b/internal/ingest/config/stellar-core_testnet.cfg deleted file mode 100644 index 357470a..0000000 --- a/internal/ingest/config/stellar-core_testnet.cfg +++ /dev/null @@ -1,25 +0,0 @@ -# Stellar Testnet validators -[[HOME_DOMAINS]] -HOME_DOMAIN="testnet.stellar.org" -QUALITY="HIGH" - -[[VALIDATORS]] -NAME="sdftest1" -HOME_DOMAIN="testnet.stellar.org" -PUBLIC_KEY="GDKXE2OZMJIPOSLNA6N6F2BVCI3O777I2OOC4BV7VOYUEHYX7RTRYA7Y" -ADDRESS="core-testnet1.stellar.org" -HISTORY="curl -sf http://history.stellar.org/prd/core-testnet/core_testnet_001/{0} -o {1}" - -[[VALIDATORS]] -NAME="sdftest2" -HOME_DOMAIN="testnet.stellar.org" -PUBLIC_KEY="GCUCJTIYXSOXKBSNFGNFWW5MUQ54HKRPGJUTQFJ5RQXZXNOLNXYDHRAP" -ADDRESS="core-testnet2.stellar.org" -HISTORY="curl -sf http://history.stellar.org/prd/core-testnet/core_testnet_002/{0} -o {1}" - -[[VALIDATORS]] -NAME="sdftest3" -HOME_DOMAIN="testnet.stellar.org" -PUBLIC_KEY="GC2V2EFSXN6SQTWVYA5EPJPBWWIMSD2XQNKUOHGEKB535AQE2I6IXV2Z" -ADDRESS="core-testnet3.stellar.org" -HISTORY="curl -sf http://history.stellar.org/prd/core-testnet/core_testnet_003/{0} -o {1}" \ No newline at end of file diff --git a/internal/ingest/ingest.go b/internal/ingest/ingest.go index b56cdc8..2d8272a 100644 --- a/internal/ingest/ingest.go +++ b/internal/ingest/ingest.go @@ -2,102 +2,51 @@ package ingest import ( "context" - "errors" "fmt" "net/http" - "os" - "path" "time" "github.com/sirupsen/logrus" - "github.com/stellar/go/ingest/ledgerbackend" - "github.com/stellar/go/network" "github.com/stellar/go/support/log" "github.com/stellar/wallet-backend/internal/apptracker" "github.com/stellar/wallet-backend/internal/data" "github.com/stellar/wallet-backend/internal/db" "github.com/stellar/wallet-backend/internal/services" - tssservices "github.com/stellar/wallet-backend/internal/tss/utils" + "github.com/stellar/wallet-backend/internal/tss" + tssrouter "github.com/stellar/wallet-backend/internal/tss/router" + tssstore "github.com/stellar/wallet-backend/internal/tss/store" ) -// Change configs to have router, transactionservice, StartingLedger, store (to look up the transaction xdr/hash to change status), AppTracker - -type RPCConfigs struct { - LedgerCursorName string - RPCCursorName string - StartLedger int - StartCursor string - DatabaseURL string - LogLevel logrus.Level - AppTracker apptracker.AppTracker - RPCURL string -} - type Configs struct { - DatabaseURL string - NetworkPassphrase string - CaptiveCoreBinPath string - CaptiveCoreConfigDir string - LedgerCursorName string - StartLedger int - EndLedger int - LogLevel logrus.Level - AppTracker apptracker.AppTracker - RPCURL string -} - -func RPCIngest(cfg RPCConfigs) error { - ctx := context.Background() - - manager, err := setupRPCDeps(cfg) - if err != nil { - log.Ctx(ctx).Fatalf("Error setting up dependencies for ingest: %v", err) - } - - if err = manager.Run(ctx, uint32(cfg.StartLedger), cfg.StartCursor); err != nil { - log.Ctx(ctx).Fatalf("Running ingest from start ledger: %d, start cursor: %d: %v", cfg.StartLedger, cfg.StartCursor, err) - } - - return nil + DatabaseURL string + LedgerCursorName string + StartLedger int + EndLedger int + LogLevel logrus.Level + AppTracker apptracker.AppTracker + RPCURL string + WebhookChannelMaxBufferSize int + WebhookChannelMaxWorkers int + WebhookChannelMaxRetries int + WebhookChannelWaitBtwnTriesMS int + WebhookChannel tss.Channel } func Ingest(cfg Configs) error { ctx := context.Background() - manager, err := setupDeps(cfg) + ingestService, err := setupDeps(cfg) if err != nil { log.Ctx(ctx).Fatalf("Error setting up dependencies for ingest: %v", err) } - if err = manager.Run(ctx, uint32(cfg.StartLedger), uint32(cfg.EndLedger)); err != nil { - log.Ctx(ctx).Fatalf("Running ingest from %d to %d: %v", cfg.StartLedger, cfg.EndLedger, err) + if err = ingestService.Run(ctx, uint32(cfg.StartLedger), uint32(cfg.EndLedger)); err != nil { + log.Ctx(ctx).Fatalf("Running ingest from %d to %d: %w", cfg.StartLedger, cfg.EndLedger) } return nil } -func setupRPCDeps(cfg RPCConfigs) (*services.RPCIngestManager, error) { - dbConnectionPool, err := db.OpenDBConnectionPool(cfg.DatabaseURL) - if err != nil { - return nil, fmt.Errorf("error connecting to the database: %w", err) - } - models, err := data.NewModels(dbConnectionPool) - if err != nil { - return nil, fmt.Errorf("error creating models for Serve: %w", err) - } - httpClient := http.Client{Timeout: time.Duration(30 * time.Second)} - txServiceOpts := tssservices.TransactionServiceOptions{RPCURL: cfg.RPCURL, HTTPClient: &httpClient} - - txService, err := tssservices.NewTransactionService(txServiceOpts) - return &services.RPCIngestManager{ - PaymentModel: models.Payments, - AppTracker: cfg.AppTracker, - TransactionService: txService, - LedgerCursorName: cfg.LedgerCursorName, - RPCCursorName: cfg.RPCCursorName, - }, nil -} - func setupDeps(cfg Configs) (services.IngestService, error) { // Open DB connection pool dbConnectionPool, err := db.OpenDBConnectionPool(cfg.DatabaseURL) @@ -108,71 +57,23 @@ func setupDeps(cfg Configs) (services.IngestService, error) { if err != nil { return nil, fmt.Errorf("creating models: %w", err) } - - // Setup Captive Core backend - captiveCoreConfig, err := getCaptiveCoreConfig(cfg) - if err != nil { - return nil, fmt.Errorf("getting captive core config: %w", err) - } - ledgerBackend, err := ledgerbackend.NewCaptive(captiveCoreConfig) - if err != nil { - return nil, fmt.Errorf("creating captive core backend: %w", err) - } - httpClient := &http.Client{Timeout: 30 * time.Second} rpcService, err := services.NewRPCService(cfg.RPCURL, httpClient) if err != nil { return nil, fmt.Errorf("instantiating rpc service: %w", err) } - - ingestService, err := services.NewIngestService(models, ledgerBackend, cfg.NetworkPassphrase, cfg.LedgerCursorName, cfg.AppTracker, rpcService) - if err != nil { - return nil, fmt.Errorf("instantiating ingest service: %w", err) + tssStore := tssstore.NewStore(dbConnectionPool) + tssRouterConfig := tssrouter.RouterConfigs{ + WebhookChannel: cfg.WebhookChannel, } - return ingestService, nil -} - -const ( - configFileNamePubnet = "stellar-core_pubnet.cfg" - configFileNameTestnet = "stellar-core_testnet.cfg" -) - -func getCaptiveCoreConfig(cfg Configs) (ledgerbackend.CaptiveCoreConfig, error) { - var networkArchivesURLs []string - var configFilePath string - - switch cfg.NetworkPassphrase { - case network.TestNetworkPassphrase: - networkArchivesURLs = network.TestNetworkhistoryArchiveURLs - configFilePath = path.Join(cfg.CaptiveCoreConfigDir, configFileNameTestnet) - case network.PublicNetworkPassphrase: - networkArchivesURLs = network.PublicNetworkhistoryArchiveURLs - configFilePath = path.Join(cfg.CaptiveCoreConfigDir, configFileNamePubnet) - default: - return ledgerbackend.CaptiveCoreConfig{}, fmt.Errorf("unknown network: %s", cfg.NetworkPassphrase) - } - - if _, err := os.Stat(configFilePath); errors.Is(err, os.ErrNotExist) { - return ledgerbackend.CaptiveCoreConfig{}, fmt.Errorf("captive core configuration file not found in %s", configFilePath) - } + router := tssrouter.NewRouter(tssRouterConfig) - // Read configuration TOML - captiveCoreToml, err := ledgerbackend.NewCaptiveCoreTomlFromFile(configFilePath, ledgerbackend.CaptiveCoreTomlParams{ - CoreBinaryPath: cfg.CaptiveCoreBinPath, - NetworkPassphrase: cfg.NetworkPassphrase, - HistoryArchiveURLs: networkArchivesURLs, - UseDB: true, - }) + ingestService, err := services.NewIngestService( + models, cfg.LedgerCursorName, cfg.AppTracker, rpcService, router, tssStore) if err != nil { - return ledgerbackend.CaptiveCoreConfig{}, fmt.Errorf("creating captive core toml: %w", err) + return nil, fmt.Errorf("instantiating ingest service: %w", err) } - return ledgerbackend.CaptiveCoreConfig{ - NetworkPassphrase: cfg.NetworkPassphrase, - HistoryArchiveURLs: networkArchivesURLs, - BinaryPath: cfg.CaptiveCoreBinPath, - Toml: captiveCoreToml, - UseDB: true, - }, nil + return ingestService, nil } diff --git a/internal/ingest/ingest_test.go b/internal/ingest/ingest_test.go deleted file mode 100644 index 5bf4c59..0000000 --- a/internal/ingest/ingest_test.go +++ /dev/null @@ -1,61 +0,0 @@ -package ingest - -import ( - "testing" - - "github.com/stellar/go/network" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestGetCaptiveCoreConfig(t *testing.T) { - t.Run("testnet_success", func(t *testing.T) { - config, err := getCaptiveCoreConfig(Configs{ - NetworkPassphrase: network.TestNetworkPassphrase, - CaptiveCoreBinPath: "/bin/path", - CaptiveCoreConfigDir: "./config", - }) - - require.NoError(t, err) - assert.Equal(t, "/bin/path", config.BinaryPath) - assert.Equal(t, network.TestNetworkPassphrase, config.NetworkPassphrase) - assert.Equal(t, network.TestNetworkhistoryArchiveURLs, config.HistoryArchiveURLs) - assert.Equal(t, true, config.UseDB) - assert.NotNil(t, config.Toml) - }) - - t.Run("pubnet_success", func(t *testing.T) { - config, err := getCaptiveCoreConfig(Configs{ - NetworkPassphrase: network.PublicNetworkPassphrase, - CaptiveCoreBinPath: "/bin/path", - CaptiveCoreConfigDir: "./config", - }) - - require.NoError(t, err) - assert.Equal(t, "/bin/path", config.BinaryPath) - assert.Equal(t, network.PublicNetworkPassphrase, config.NetworkPassphrase) - assert.Equal(t, network.PublicNetworkhistoryArchiveURLs, config.HistoryArchiveURLs) - assert.Equal(t, true, config.UseDB) - assert.NotNil(t, config.Toml) - }) - - t.Run("unknown_network", func(t *testing.T) { - _, err := getCaptiveCoreConfig(Configs{ - NetworkPassphrase: "Invalid SDF Network ; May 2024", - CaptiveCoreBinPath: "/bin/path", - CaptiveCoreConfigDir: "./config", - }) - - assert.ErrorContains(t, err, "unknown network: Invalid SDF Network ; May 2024") - }) - - t.Run("invalid_config_file", func(t *testing.T) { - _, err := getCaptiveCoreConfig(Configs{ - NetworkPassphrase: network.TestNetworkPassphrase, - CaptiveCoreBinPath: "/bin/path", - CaptiveCoreConfigDir: "./invalid/path", - }) - - assert.ErrorContains(t, err, "captive core configuration file not found in invalid/path/stellar-core_testnet.cfg") - }) -} diff --git a/internal/services/ingest.go b/internal/services/ingest.go index 7db4b9c..6bffe5e 100644 --- a/internal/services/ingest.go +++ b/internal/services/ingest.go @@ -4,113 +4,48 @@ import ( "context" "errors" "fmt" - "io" "time" "github.com/stellar/go/ingest" - "github.com/stellar/go/ingest/ledgerbackend" "github.com/stellar/go/support/log" + "github.com/stellar/go/txnbuild" "github.com/stellar/go/xdr" "github.com/stellar/wallet-backend/internal/apptracker" "github.com/stellar/wallet-backend/internal/data" "github.com/stellar/wallet-backend/internal/db" + "github.com/stellar/wallet-backend/internal/entities" + "github.com/stellar/wallet-backend/internal/tss" + tssrouter "github.com/stellar/wallet-backend/internal/tss/router" + tssstore "github.com/stellar/wallet-backend/internal/tss/store" "github.com/stellar/wallet-backend/internal/utils" ) -type RPCIngestManager struct { - RPCService RPCService - PaymentModel *data.PaymentModel - AppTracker apptracker.AppTracker - LedgerCursorName string - RPCCursorName string -} - -type IngestManager struct { - PaymentModel *data.PaymentModel - LedgerBackend ledgerbackend.LedgerBackend - NetworkPassphrase string - LedgerCursorName string - AppTracker apptracker.AppTracker -} - -/* -func (m *RPCIngestManager) Run(ctx context.Context, startLedger uint32, startCursor string) error { - heartbeat := make(chan any) - go trackServiceHealth(heartbeat, m.AppTracker) - cursor := "" - ledger := 0 - if startCursor != "" { - cursor = startCursor - } else if startLedger != 0 { - ledger = int(startLedger) - } else { - lastSyncedCursor, err := m.PaymentModel.GetLatestLedgerSynced(ctx, m.RPCCursorName) - if err != nil { - return fmt.Errorf("getting last cursor synced: %w", err) - } - if lastSyncedCursor == 0 { - lastSyncedLedger, err := m.PaymentModel.GetLatestLedgerSynced(ctx, m.LedgerCursorName) - if err != nil { - return fmt.Errorf("getting last ledger synced: %w", err) - } - ledger = int(lastSyncedLedger) - } - cursor = strconv.FormatUint(uint64(lastSyncedCursor), 10) - } - - for { - time.Sleep(10) - txns, cursor, err := m.TransactionService.GetTransactions(ledger, cursor, 200) - if err != nil { - return fmt.Errorf("getTransactions: %w", err) - } - heartbeat <- true - iCursor, err := strconv.ParseUint(cursor, 10, 32) - if err != nil { - return fmt.Errorf("cannot convert cursor to int: %s", err.Error()) - } - err = m.PaymentModel.UpdateLatestLedgerSynced(ctx, m.RPCCursorName, uint32(iCursor)) - if err != nil { - return err - } - m.processGetTransactionsResponse(ctx, txns) - } -} -*/ - -// func (m *IngestManager) Run(ctx context.Context, start, end uint32) error { type IngestService interface { - Run(ctx context.Context, start, end uint32) error + Run(ctx context.Context, startLedger uint32, endLedger uint32) error } var _ IngestService = (*ingestService)(nil) type ingestService struct { - models *data.Models - ledgerBackend ledgerbackend.LedgerBackend - networkPassphrase string - ledgerCursorName string - appTracker apptracker.AppTracker - rpcService RPCService + models *data.Models + ledgerCursorName string + appTracker apptracker.AppTracker + rpcService RPCService + tssRouter tssrouter.Router + tssStore tssstore.Store } func NewIngestService( models *data.Models, - ledgerBackend ledgerbackend.LedgerBackend, - networkPassphrase string, ledgerCursorName string, appTracker apptracker.AppTracker, rpcService RPCService, + tssRouter tssrouter.Router, + tssStore tssstore.Store, ) (*ingestService, error) { if models == nil { return nil, errors.New("models cannot be nil") } - if ledgerBackend == nil { - return nil, errors.New("ledgerBackend cannot be nil") - } - if networkPassphrase == "" { - return nil, errors.New("networkPassphrase cannot be nil") - } if ledgerCursorName == "" { return nil, errors.New("ledgerCursorName cannot be nil") } @@ -120,122 +55,87 @@ func NewIngestService( if rpcService == nil { return nil, errors.New("rpcService cannot be nil") } + if tssRouter == nil { + return nil, errors.New("tssRouter cannot be nil") + } + if tssStore == nil { + return nil, errors.New("tssStore cannot be nil") + } return &ingestService{ - models: models, - ledgerBackend: ledgerBackend, - networkPassphrase: networkPassphrase, - ledgerCursorName: ledgerCursorName, - appTracker: appTracker, - rpcService: rpcService, + models: models, + ledgerCursorName: ledgerCursorName, + appTracker: appTracker, + rpcService: rpcService, + tssRouter: tssRouter, + tssStore: tssStore, }, nil } -func (m *ingestService) Run(ctx context.Context, start, end uint32) error { - var ingestLedger uint32 - - if start == 0 { - lastSyncedLedger, err := m.models.Payments.GetLatestLedgerSynced(ctx, m.ledgerCursorName) +func (m *ingestService) Run(ctx context.Context, startLedger uint32, endLedger uint32) error { + heartbeat := make(chan any) + go trackServiceHealth(heartbeat, m.appTracker) + if startLedger == 0 { + var err error + startLedger, err = m.models.Payments.GetLatestLedgerSynced(ctx, m.ledgerCursorName) if err != nil { - return fmt.Errorf("getting last ledger synced: %w", err) - } - - if lastSyncedLedger == 0 { - // Captive Core is not able to process genesis ledger (1) and often has trouble processing ledger 2, so we start ingestion at ledger 3 - log.Ctx(ctx).Info("No last synced ledger cursor found, initializing ingestion at ledger 3") - ingestLedger = 3 - } else { - ingestLedger = lastSyncedLedger + 1 + return fmt.Errorf("erorr getting start ledger: %w", err) } - } else { - ingestLedger = start - } - - if end != 0 && ingestLedger > end { - return fmt.Errorf("starting ledger (%d) may not be greater than ending ledger (%d)", ingestLedger, end) - } - - err := m.maybePrepareRange(ctx, ingestLedger, end) - if err != nil { - return fmt.Errorf("preparing range from %d to %d: %w", ingestLedger, end, err) } - - heartbeat := make(chan any) - go trackServiceHealth(heartbeat, m.appTracker) - - for ; end == 0 || ingestLedger <= end; ingestLedger++ { - log.Ctx(ctx).Infof("waiting for ledger %d", ingestLedger) - - ledgerMeta, err := m.ledgerBackend.GetLedger(ctx, ingestLedger) + ingestLedger := startLedger + for ; endLedger == 0 || ingestLedger <= endLedger; ingestLedger++ { + time.Sleep(10 * time.Second) + ledgerTransactions, err := m.GetLedgerTransactions(int64(ingestLedger)) if err != nil { - return fmt.Errorf("getting ledger meta for ledger %d: %w", ingestLedger, err) + return fmt.Errorf("getTransactions: %w", err) } - heartbeat <- true - - err = m.processLedger(ctx, ingestLedger, ledgerMeta) + err = m.ingestPayments(ctx, ledgerTransactions) if err != nil { - return fmt.Errorf("processing ledger %d: %w", ingestLedger, err) + return fmt.Errorf("error ingesting payments: %w", err) } - - log.Ctx(ctx).Infof("ledger %d successfully processed", ingestLedger) - } - - return nil -} - -func (m *ingestService) maybePrepareRange(ctx context.Context, from, to uint32) error { - var ledgerRange ledgerbackend.Range - if to == 0 { - ledgerRange = ledgerbackend.UnboundedRange(from) - } else { - ledgerRange = ledgerbackend.BoundedRange(from, to) - } - - prepared, err := m.ledgerBackend.IsPrepared(ctx, ledgerRange) - if err != nil { - return fmt.Errorf("checking prepared range: %w", err) - } - - if !prepared { - err = m.ledgerBackend.PrepareRange(ctx, ledgerRange) + err = m.processTSSTransactions(ctx, ledgerTransactions) if err != nil { - return fmt.Errorf("preparing range: %w", err) + return fmt.Errorf("error processing tss transactions: %w", err) + } + err = m.models.Payments.UpdateLatestLedgerSynced(ctx, m.ledgerCursorName, uint32(ingestLedger)) + if err != nil { + return fmt.Errorf("error updating latest synced ledger: %w", err) } } - return nil } -func trackServiceHealth(heartbeat chan any, tracker apptracker.AppTracker) { - const alertAfter = time.Second * 60 - ticker := time.NewTicker(alertAfter) +func (m *ingestService) GetLedgerTransactions(ledger int64) ([]entities.Transaction, error) { - for { - select { - case <-ticker.C: - warn := fmt.Sprintf("ingestion service stale for over %s", alertAfter) - log.Warn(warn) - if tracker != nil { - tracker.CaptureMessage(warn) + var ledgerTransactions []entities.Transaction + var cursor string + lastLedgerSeen := ledger + for lastLedgerSeen == ledger { + getTxnsResp, err := m.rpcService.GetTransactions(ledger, cursor, 50) + if err != nil { + return []entities.Transaction{}, fmt.Errorf("getTransactions: %w", err) + } + cursor = getTxnsResp.Cursor + for _, tx := range getTxnsResp.Transactions { + if tx.Ledger == ledger { + ledgerTransactions = append(ledgerTransactions, tx) + lastLedgerSeen = tx.Ledger } else { - log.Warn("App Tracker is nil") + lastLedgerSeen = tx.Ledger + break } - ticker.Reset(alertAfter) - case <-heartbeat: - ticker.Reset(alertAfter) } } + return ledgerTransactions, nil } -/* -func (m *RPCIngestManager) processGetTransactionsResponse(ctx context.Context, txns []tss.RPCGetIngestTxResponse) (err error) { - return db.RunInTransaction(ctx, m.PaymentModel.DB, nil, func(dbTx db.Transaction) error { - for _, tx := range txns { - if tx.Status != tss.SuccessStatus { +func (m *ingestService) ingestPayments(ctx context.Context, ledgerTransactions []entities.Transaction) error { + return db.RunInTransaction(ctx, m.models.Payments.DB, nil, func(dbTx db.Transaction) error { + for _, tx := range ledgerTransactions { + if tx.Status != entities.SuccessStatus { continue } - genericTx, err := txnbuild.TransactionFromXDR(tx.EnvelopeXDR) if err != nil { return fmt.Errorf("deserializing envelope xdr: %w", err) @@ -244,14 +144,12 @@ func (m *RPCIngestManager) processGetTransactionsResponse(ctx context.Context, t if err != nil { return fmt.Errorf("generic transaction cannot be unpacked into a transaction") } - txResultXDR, err := m.TransactionService.UnmarshalTransactionResultXDR(tx.ResultXDR) + txResultXDR, err := tss.UnmarshallTransactionResultXDR(tx.ResultXDR) if err != nil { return fmt.Errorf("cannot unmarshal transacation result xdr: %s", err.Error()) } - txHash := "" - - txMemo, txMemoType := utils.Memo(txEnvelopeXDR.Memo(), "") + txMemo, txMemoType := utils.Memo(txEnvelopeXDR.Memo(), tx.Hash) if txMemo != nil { *txMemo = utils.SanitizeUTF8(*txMemo) } @@ -263,7 +161,7 @@ func (m *RPCIngestManager) processGetTransactionsResponse(ctx context.Context, t OperationID: utils.OperationID(int32(tx.Ledger), int32(tx.ApplicationOrder), int32(opIdx)), OperationType: op.Body.Type.String(), TransactionID: utils.TransactionID(int32(tx.Ledger), int32(tx.ApplicationOrder)), - TransactionHash: txHash, + TransactionHash: tx.Hash, FromAddress: utils.SourceAccountRPC(op, txEnvelopeXDR), CreatedAt: time.Unix(tx.CreatedAt, 0), Memo: txMemo, @@ -281,85 +179,79 @@ func (m *RPCIngestManager) processGetTransactionsResponse(ctx context.Context, t continue } } - } return nil }) } -*/ -func (m *IngestManager) processLedger(ctx context.Context, ledger uint32, ledgerMeta xdr.LedgerCloseMeta) (err error) { - reader, err := ingest.NewLedgerTransactionReaderFromLedgerCloseMeta(m.NetworkPassphrase, ledgerMeta) - if err != nil { - return fmt.Errorf("creating ledger reader: %w", err) - } - - ledgerCloseTime := time.Unix(int64(ledgerMeta.LedgerHeaderHistoryEntry().Header.ScpValue.CloseTime), 0).UTC() - ledgerSequence := ledgerMeta.LedgerSequence() - - return db.RunInTransaction(ctx, m.models.Payments.DB, nil, func(dbTx db.Transaction) error { - for { - tx, err := reader.Read() - if err == io.EOF { - break - } - if err != nil { - return fmt.Errorf("reading transaction: %w", err) - } - - if !tx.Result.Successful() { - continue - } - - // tx = deserialized envelopeXdr tx.Envelope.Memo() is now envelope.Memo() - // tx.Index = applicationOrder - GetTransactions has to return this. May have to update GetTransaction to get this too - // cant I just use the transaction hash here? - txHash := utils.TransactionHash(ledgerMeta, int(tx.Index)) - txMemo, txMemoType := utils.Memo(tx.Envelope.Memo(), txHash) - // The memo field is subject to user input, so we sanitize before persisting in the database - if txMemo != nil { - *txMemo = utils.SanitizeUTF8(*txMemo) - } - - for idx, op := range tx.Envelope.Operations() { - opIdx := idx + 1 - - payment := data.Payment{ - OperationID: utils.OperationID(int32(ledgerSequence), int32(tx.Index), int32(opIdx)), - OperationType: op.Body.Type.String(), - TransactionID: utils.TransactionID(int32(ledgerSequence), int32(tx.Index)), - TransactionHash: txHash, - FromAddress: utils.SourceAccount(op, tx), - CreatedAt: ledgerCloseTime, - Memo: txMemo, - MemoType: txMemoType, - } +func (m *ingestService) processTSSTransactions(ctx context.Context, ledgerTransactions []entities.Transaction) error { + for _, tx := range ledgerTransactions { + if !tx.FeeBump { + // because all transactions submitted by TSS are fee bump transactions + continue + } + tssTry, err := m.tssStore.GetTry(ctx, tx.Hash) + if err != nil { + return fmt.Errorf("error when getting try: %w", err) + } + if tssTry == (tssstore.Try{}) { + continue + } - switch op.Body.Type { - case xdr.OperationTypePayment: - fillPayment(&payment, op.Body) - case xdr.OperationTypePathPaymentStrictSend: - fillPathSend(&payment, op.Body, tx, opIdx) - case xdr.OperationTypePathPaymentStrictReceive: - fillPathReceive(&payment, op.Body, tx, opIdx) - default: - continue - } + transaction, err := m.tssStore.GetTransaction(ctx, tssTry.OrigTxHash) + if err != nil { + return fmt.Errorf("error getting transaction: %w", err) + } + status := tss.RPCTXStatus{RPCStatus: tx.Status} + code, err := tss.TransactionResultXDRToCode(tx.ResultXDR) + if err != nil { + return fmt.Errorf("error unmarshaling resultxdr: %w", err) + } + m.tssStore.UpsertTry(ctx, tssTry.OrigTxHash, tssTry.Hash, tssTry.XDR, code) + m.tssStore.UpsertTransaction(ctx, transaction.WebhookURL, tssTry.OrigTxHash, transaction.XDR, status) - err = m.models.Payments.AddPayment(ctx, dbTx, payment) - if err != nil { - return fmt.Errorf("adding payment for ledger %d, tx %s (%d), operation %s (%d): %w", ledgerSequence, txHash, tx.Index, payment.OperationID, opIdx, err) - } - } + txCode, err := tss.TransactionResultXDRToCode(tx.ResultXDR) + if err != nil { + return fmt.Errorf("unable to extract tx code from result xdr string: %w", err) } - err = m.models.Payments.UpdateLatestLedgerSynced(ctx, m.ledgerCursorName, ledger) + tssGetIngestResponse := tss.RPCGetIngestTxResponse{ + Status: tx.Status, + Code: txCode, + EnvelopeXDR: tx.EnvelopeXDR, + ResultXDR: tx.ResultXDR, + CreatedAt: tx.CreatedAt, + } + payload := tss.Payload{ + RpcGetIngestTxResponse: tssGetIngestResponse, + } + err = m.tssRouter.Route(payload) if err != nil { - return err + return fmt.Errorf("unable to route payload: %w", err) } + } + return nil +} - return nil - }) +func trackServiceHealth(heartbeat chan any, tracker apptracker.AppTracker) { + const alertAfter = time.Second * 60 + ticker := time.NewTicker(alertAfter) + + for { + select { + case <-ticker.C: + warn := fmt.Sprintf("ingestion service stale for over %s", alertAfter) + log.Warn(warn) + if tracker != nil { + tracker.CaptureMessage(warn) + } else { + log.Warn("App Tracker is nil") + } + ticker.Reset(alertAfter) + case <-heartbeat: + ticker.Reset(alertAfter) + } + } } func fillPayment(payment *data.Payment, operation xdr.OperationBody) { diff --git a/internal/services/ingest_test.go b/internal/services/ingest_test.go index 4e8da50..11c2ae4 100644 --- a/internal/services/ingest_test.go +++ b/internal/services/ingest_test.go @@ -3,145 +3,158 @@ package services import ( "context" "testing" - "time" - "github.com/stellar/go/network" "github.com/stellar/go/xdr" + "github.com/stellar/wallet-backend/internal/apptracker" "github.com/stellar/wallet-backend/internal/data" "github.com/stellar/wallet-backend/internal/db" "github.com/stellar/wallet-backend/internal/db/dbtest" - "github.com/stellar/wallet-backend/internal/utils" + "github.com/stellar/wallet-backend/internal/entities" + "github.com/stellar/wallet-backend/internal/tss" + tssrouter "github.com/stellar/wallet-backend/internal/tss/router" + tssstore "github.com/stellar/wallet-backend/internal/tss/store" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) -func TestProcessLedger(t *testing.T) { +func TestGetLedgerTransactions(t *testing.T) { dbt := dbtest.Open(t) defer dbt.Close() dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) require.NoError(t, err) defer dbConnectionPool.Close() - models, _ := data.NewModels(dbConnectionPool) - service := &ingestService{ - models: models, - networkPassphrase: network.TestNetworkPassphrase, - ledgerCursorName: "last_synced_ledger", - ledgerBackend: nil, - rpcService: nil, - } - - ctx := context.Background() - - // Insert destination account into subscribed addresses - destinationAccount := "GBLI2OE4H3HAW7Z2GXLYZQNQ57XLHJ5OILFPVL33EPA4GDAIQ5F33JGA" - _, err = dbConnectionPool.ExecContext(ctx, "INSERT INTO accounts (stellar_address) VALUES ($1)", destinationAccount) - require.NoError(t, err) - - ledgerMeta := xdr.LedgerCloseMeta{ - V: 1, - V1: &xdr.LedgerCloseMetaV1{ - LedgerHeader: xdr.LedgerHeaderHistoryEntry{ - Header: xdr.LedgerHeader{ - LedgerSeq: 123, - ScpValue: xdr.StellarValue{ - CloseTime: xdr.TimePoint(time.Date(2024, 5, 28, 11, 0, 0, 0, time.UTC).Unix()), - }, - LedgerVersion: 10, + mockAppTracker := apptracker.MockAppTracker{} + mockRPCService := RPCServiceMock{} + mockRouter := tssrouter.MockRouter{} + tssStore := tssstore.NewStore(dbConnectionPool) + ingestService, _ := NewIngestService(models, "ingestionLedger", &mockAppTracker, &mockRPCService, &mockRouter, tssStore) + t.Run("all_ledger_transactions_in_single_gettransactions_call", func(t *testing.T) { + rpcGetTransactionsResult := entities.RPCGetTransactionsResult{ + Cursor: "51", + Transactions: []entities.Transaction{ + { + Status: entities.SuccessStatus, + Hash: "hash1", + Ledger: 1, + }, + { + Status: entities.FailedStatus, + Hash: "hash2", + Ledger: 2, }, }, - TxSet: xdr.GeneralizedTransactionSet{ - V1TxSet: &xdr.TransactionSetV1{ - Phases: []xdr.TransactionPhase{ - { - V0Components: &[]xdr.TxSetComponent{ - { - TxsMaybeDiscountedFee: &xdr.TxSetComponentTxsMaybeDiscountedFee{ - Txs: []xdr.TransactionEnvelope{ - { - Type: xdr.EnvelopeTypeEnvelopeTypeTx, - V1: &xdr.TransactionV1Envelope{ - Tx: xdr.Transaction{ - SourceAccount: xdr.MustMuxedAddress("GB3H2CRRTO7W5WF54K53A3MRAFEUISHZ7Y5YGRVGRGHUZESLV5VYYWXI"), - SeqNum: 321, - Memo: xdr.MemoText("memo_test"), - Operations: []xdr.Operation{ - { - SourceAccount: nil, - Body: xdr.OperationBody{ - Type: xdr.OperationTypePayment, - PaymentOp: &xdr.PaymentOp{ - Destination: xdr.MustMuxedAddress(destinationAccount), - Asset: xdr.Asset{ - Type: xdr.AssetTypeAssetTypeCreditAlphanum4, - AlphaNum4: &xdr.AlphaNum4{ - AssetCode: xdr.AssetCode4([]byte("USDC")), - Issuer: xdr.MustMuxedAddress("GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5").ToAccountId(), - }, - }, - Amount: xdr.Int64(50), - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, + } + mockRPCService. + On("GetTransactions", int64(1), "", 50). + Return(rpcGetTransactionsResult, nil). + Once() + + txns, err := ingestService.GetLedgerTransactions(1) + assert.Equal(t, 1, len(txns)) + assert.Equal(t, txns[0].Hash, "hash1") + assert.Empty(t, err) + }) + + t.Run("ledger_transactions_split_between_multiple_gettransactions_calls", func(t *testing.T) { + rpcGetTransactionsResult1 := entities.RPCGetTransactionsResult{ + Cursor: "51", + Transactions: []entities.Transaction{ + { + Status: entities.SuccessStatus, + Hash: "hash1", + Ledger: 1, + }, + { + Status: entities.FailedStatus, + Hash: "hash2", + Ledger: 1, }, }, - TxProcessing: []xdr.TransactionResultMeta{ + } + rpcGetTransactionsResult2 := entities.RPCGetTransactionsResult{ + Cursor: "51", + Transactions: []entities.Transaction{ { - Result: xdr.TransactionResultPair{ - TransactionHash: xdr.Hash{}, - }, - TxApplyProcessing: xdr.TransactionMeta{ - V: 3, - }, + Status: entities.SuccessStatus, + Hash: "hash3", + Ledger: 1, + }, + { + Status: entities.FailedStatus, + Hash: "hash4", + Ledger: 2, }, }, - }, - } + } - // Compute transaction hash and inject into ledger meta - components := ledgerMeta.V1.TxSet.V1TxSet.Phases[0].V0Components - xdrHash, err := network.HashTransactionInEnvelope((*components)[0].TxsMaybeDiscountedFee.Txs[0], service.networkPassphrase) - require.NoError(t, err) - ledgerMeta.V1.TxProcessing[0].Result.TransactionHash = xdrHash + mockRPCService. + On("GetTransactions", int64(1), "", 50). + Return(rpcGetTransactionsResult1, nil). + Once() - // Run ledger ingestion - err = service.processLedger(ctx, 1, ledgerMeta) - require.NoError(t, err) + mockRPCService. + On("GetTransactions", int64(1), "51", 50). + Return(rpcGetTransactionsResult2, nil). + Once() - // Assert payment properly persisted to database - var payment data.Payment - query := `SELECT * FROM ingest_payments` - err = dbConnectionPool.GetContext(ctx, &payment, query) + txns, err := ingestService.GetLedgerTransactions(1) + assert.Equal(t, 3, len(txns)) + assert.Equal(t, txns[0].Hash, "hash1") + assert.Equal(t, txns[1].Hash, "hash2") + assert.Equal(t, txns[2].Hash, "hash3") + assert.Empty(t, err) + }) + +} + +func TestProcessTSSTransactions(t *testing.T) { + dbt := dbtest.Open(t) + defer dbt.Close() + + dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) require.NoError(t, err) + defer dbConnectionPool.Close() + models, _ := data.NewModels(dbConnectionPool) + mockAppTracker := apptracker.MockAppTracker{} + mockRPCService := RPCServiceMock{} + mockRouter := tssrouter.MockRouter{} + tssStore := tssstore.NewStore(dbConnectionPool) + ingestService, _ := NewIngestService(models, "ingestionLedger", &mockAppTracker, &mockRPCService, &mockRouter, tssStore) + + t.Run("routes_to_tss_router", func(t *testing.T) { + + transactions := []entities.Transaction{ + { + Status: entities.SuccessStatus, + Hash: "feebumphash", + ApplicationOrder: 1, + FeeBump: true, + EnvelopeXDR: "feebumpxdr", + ResultXDR: "AAAAAAAAAMj////9AAAAAA==", + ResultMetaXDR: "meta", + Ledger: 123456, + DiagnosticEventsXDR: "diag", + CreatedAt: 1695939098, + }, + } + + tssStore.UpsertTransaction(context.Background(), "localhost:8000/webhook", "hash", "xdr", tss.RPCTXStatus{OtherStatus: tss.NewStatus}) + tssStore.UpsertTry(context.Background(), "hash", "feebumphash", "feebumpxdr", tss.RPCTXCode{OtherCodes: tss.NewCode}) + + mockRouter. + On("Route", mock.AnythingOfType("tss.Payload")). + Return(nil). + Once() + + err := ingestService.processTSSTransactions(context.Background(), transactions) + assert.Empty(t, err) - assert.Equal(t, data.Payment{ - OperationID: "528280981505", - OperationType: xdr.OperationTypePayment.String(), - TransactionID: "528280981504", - TransactionHash: "c20936e363c85799b31fd321b67aa49ecd88f04fc41297959387e445245080db", - FromAddress: "GB3H2CRRTO7W5WF54K53A3MRAFEUISHZ7Y5YGRVGRGHUZESLV5VYYWXI", - ToAddress: "GBLI2OE4H3HAW7Z2GXLYZQNQ57XLHJ5OILFPVL33EPA4GDAIQ5F33JGA", - SrcAssetCode: "USDC", - SrcAssetIssuer: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", - SrcAssetType: xdr.AssetTypeAssetTypeCreditAlphanum4.String(), - SrcAmount: 50, - DestAssetCode: "USDC", - DestAssetIssuer: "GBBD47IF6LWK7P7MDEVSCWR7DPUWV3NY3DTQEVFL4NAT4AQH3ZLLFLA5", - DestAssetType: xdr.AssetTypeAssetTypeCreditAlphanum4.String(), - DestAmount: 50, - CreatedAt: time.Date(2024, 5, 28, 11, 0, 0, 0, time.UTC), - Memo: utils.PointOf("memo_test"), - MemoType: xdr.MemoTypeMemoText.String(), - }, payment) + updatedTX, _ := tssStore.GetTransaction(context.Background(), "hash") + assert.Equal(t, string(entities.SuccessStatus), updatedTX.Status) + updatedTry, _ := tssStore.GetTry(context.Background(), "feebumphash") + assert.Equal(t, int32(xdr.TransactionResultCodeTxTooLate), updatedTry.Code) + }) } diff --git a/internal/services/servicesmocks/rpc_service_mocks.go b/internal/services/mocks.go similarity index 66% rename from internal/services/servicesmocks/rpc_service_mocks.go rename to internal/services/mocks.go index f72c348..a0d6175 100644 --- a/internal/services/servicesmocks/rpc_service_mocks.go +++ b/internal/services/mocks.go @@ -1,8 +1,7 @@ -package servicesmocks +package services import ( "github.com/stellar/wallet-backend/internal/entities" - "github.com/stellar/wallet-backend/internal/services" "github.com/stretchr/testify/mock" ) @@ -10,7 +9,7 @@ type RPCServiceMock struct { mock.Mock } -var _ services.RPCService = (*RPCServiceMock)(nil) +var _ RPCService = (*RPCServiceMock)(nil) func (r *RPCServiceMock) SendTransaction(transactionXdr string) (entities.RPCSendTransactionResult, error) { args := r.Called(transactionXdr) @@ -22,6 +21,11 @@ func (r *RPCServiceMock) GetTransaction(transactionHash string) (entities.RPCGet return args.Get(0).(entities.RPCGetTransactionResult), args.Error(1) } +func (r *RPCServiceMock) GetTransactions(startLedger int64, startCursor string, limit int) (entities.RPCGetTransactionsResult, error) { + args := r.Called(startLedger, startCursor, limit) + return args.Get(0).(entities.RPCGetTransactionsResult), args.Error(1) +} + type TransactionManagerMock struct { mock.Mock } diff --git a/internal/services/rpc_service.go b/internal/services/rpc_service.go index 8bebecc..1b9dce2 100644 --- a/internal/services/rpc_service.go +++ b/internal/services/rpc_service.go @@ -13,6 +13,7 @@ import ( type RPCService interface { GetTransaction(transactionHash string) (entities.RPCGetTransactionResult, error) + GetTransactions(startLedger int64, startCursor string, limit int) (entities.RPCGetTransactionsResult, error) SendTransaction(transactionXDR string) (entities.RPCSendTransactionResult, error) } @@ -55,18 +56,16 @@ func (r *rpcService) GetTransaction(transactionHash string) (entities.RPCGetTran return result, nil } -func (r *rpcService) GetTransactions(startLedger int, startCursor string, limit int) (entities.RPCGetTransactionsResult, error) { +func (r *rpcService) GetTransactions(startLedger int64, startCursor string, limit int) (entities.RPCGetTransactionsResult, error) { if limit > PageLimit { return entities.RPCGetTransactionsResult{}, fmt.Errorf("limit cannot exceed") } params := entities.RPCParams{} if startCursor != "" { - pagination := entities.RPCPagination{Cursor: startCursor, Limit: limit} - params.Pagination = pagination + params.Pagination = entities.RPCPagination{Cursor: startCursor, Limit: limit} } else { - pagination := entities.RPCPagination{Limit: limit} - params.Pagination = pagination params.StartLedger = startLedger + params.Pagination = entities.RPCPagination{Limit: limit} } resultBytes, err := r.sendRPCRequest("getTransactions", params) if err != nil { @@ -109,7 +108,6 @@ func (r *rpcService) sendRPCRequest(method string, params entities.RPCParams) (j if err != nil { return nil, fmt.Errorf("marshaling payload") } - resp, err := r.httpClient.Post(r.rpcURL, "application/json", bytes.NewBuffer(jsonData)) if err != nil { return nil, fmt.Errorf("sending POST request to RPC: %w", err) @@ -126,8 +124,6 @@ func (r *rpcService) sendRPCRequest(method string, params entities.RPCParams) (j if err != nil { return nil, fmt.Errorf("parsing RPC response JSON: %w", err) } - fmt.Println("RPC RESULT") - fmt.Println(res) if res.Result == nil { return nil, fmt.Errorf("response %s missing result field", string(body)) diff --git a/internal/services/rpc_service_test.go b/internal/services/rpc_service_test.go index ea51956..a9e27fc 100644 --- a/internal/services/rpc_service_test.go +++ b/internal/services/rpc_service_test.go @@ -9,7 +9,6 @@ import ( "net/http" "strings" "testing" - "time" "github.com/stellar/wallet-backend/internal/entities" "github.com/stellar/wallet-backend/internal/utils" @@ -28,34 +27,6 @@ func (e *errorReader) Close() error { return nil } -func TestRPCCalls(t *testing.T) { - httpClient := http.Client{Timeout: time.Duration(30 * time.Second)} - rpcURL := "http://localhost:8000/soroban/rpc" - rpcService, _ := NewRPCService(rpcURL, &httpClient) - - // SendTransaction - txXDR := "AAAAAgAAAAAVdLFaRzu3r8PAYnF6HoZDvlLId7GDj5q2gfvqMv8GKgAAAGQAAlQIAAAAAQAAAAEAAAAAAAAAAAAAAABm9fgPAAAAAAAAAAEAAAAAAAAAAQAAAACCUVwoK4/wAdfrrDuA0n4x4DVybhDKwSejzetRpCNoFwAAAAAAAAAAAJiWgAAAAAAAAAAA" - resp1, _ := rpcService.SendTransaction(txXDR) - fmt.Println("SEND TX RESPONSE") - fmt.Println(resp1) - //fmt.Println(err) - - // GetTransaction - txHash := "784fec9d8ea31d050874bb09340e662394f618a5391c7aa15f8565756304acc1" - resp2, _ := rpcService.GetTransaction(txHash) - fmt.Println("GET TX RESPONSE") - fmt.Println(resp2) - //fmt.Println(err.Error()) - - // GetTransactions - ledger := 152761 - resp3, err := rpcService.GetTransactions(ledger, "", 50) - fmt.Println("GET TXS RESPONSE") - fmt.Println(resp3) - fmt.Println(err) - -} - func TestSendRPCRequest(t *testing.T) { mockHTTPClient := utils.MockHTTPClient{} rpcURL := "http://api.vibrantapp.com/soroban/rpc" @@ -287,3 +258,66 @@ func TestGetTransaction(t *testing.T) { assert.Equal(t, "sending getTransaction request: sending POST request to RPC: connection failed", err.Error()) }) } + +func TestGetTransactions(t *testing.T) { + mockHTTPClient := utils.MockHTTPClient{} + rpcURL := "http://api.vibrantapp.com/soroban/rpc" + rpcService, _ := NewRPCService(rpcURL, &mockHTTPClient) + + t.Run("rpc_request_fails", func(t *testing.T) { + mockHTTPClient. + On("Post", rpcURL, "application/json", mock.Anything). + Return(&http.Response{}, errors.New("connection failed")). + Once() + + result, err := rpcService.GetTransactions(10, "", 5) + require.Error(t, err) + + assert.Equal(t, entities.RPCGetTransactionsResult{}, result) + assert.Equal(t, "sending getTransactions request: sending POST request to RPC: connection failed", err.Error()) + }) + + t.Run("successful", func(t *testing.T) { + params := entities.RPCParams{StartLedger: 10, Pagination: entities.RPCPagination{Limit: 5}} + + payload := map[string]interface{}{ + "jsonrpc": "2.0", + "id": 1, + "method": "getTransactions", + "params": params, + } + jsonData, _ := json.Marshal(payload) + + httpResponse := http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader(`{ + "jsonrpc": "2.0", + "id": 8675309, + "result": { + "transactions": [ + { + "status": "SUCCESS", + "applicationOrder": 1, + "feeBump": false, + "envelopeXdr": "AAAAAgAAAACDz21Q3CTITlGqRus3/96/05EDivbtfJncNQKt64BTbAAAASwAAKkyAAXlMwAAAAEAAAAAAAAAAAAAAABmWeASAAAAAQAAABR3YWxsZXQ6MTcxMjkwNjMzNjUxMAAAAAEAAAABAAAAAIPPbVDcJMhOUapG6zf/3r/TkQOK9u18mdw1Aq3rgFNsAAAAAQAAAABwOSvou8mtwTtCkysVioO35TSgyRir2+WGqO8FShG/GAAAAAFVQUgAAAAAAO371tlrHUfK+AvmQvHje1jSUrvJb3y3wrJ7EplQeqTkAAAAAAX14QAAAAAAAAAAAeuAU2wAAABAn+6A+xXvMasptAm9BEJwf5Y9CLLQtV44TsNqS8ocPmn4n8Rtyb09SBiFoMv8isYgeQU5nAHsIwBNbEKCerusAQ==", + "resultXdr": "AAAAAAAAAGT/////AAAAAQAAAAAAAAAB////+gAAAAA=", + "resultMetaXdr": "AAAAAwAAAAAAAAACAAAAAwAc0RsAAAAAAAAAAIPPbVDcJMhOUapG6zf/3r/TkQOK9u18mdw1Aq3rgFNsAAAAF0YpYBQAAKkyAAXlMgAAAAsAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAgAAAAAAAAAAAAAAAAAAAAMAAAAAABzRGgAAAABmWd/VAAAAAAAAAAEAHNEbAAAAAAAAAACDz21Q3CTITlGqRus3/96/05EDivbtfJncNQKt64BTbAAAABdGKWAUAACpMgAF5TMAAAALAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAAAAAAAAAAADAAAAAAAc0RsAAAAAZlnf2gAAAAAAAAAAAAAAAAAAAAA=", + "ledger": 1888539, + "createdAt": 1717166042 + } + ] + } + }`)), + } + + mockHTTPClient. + On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). + Return(&httpResponse, nil). + Once() + + resp, err := rpcService.GetTransactions(10, "", 5) + require.Equal(t, entities.RPCStatus("SUCCESS"), resp.Transactions[0].Status) + require.Equal(t, int64(1888539), resp.Transactions[0].Ledger) + require.NoError(t, err) + }) +} diff --git a/internal/tss/router/router.go b/internal/tss/router/router.go index 8c6801a..b974203 100644 --- a/internal/tss/router/router.go +++ b/internal/tss/router/router.go @@ -59,6 +59,8 @@ func (r *router) Route(payload tss.Payload) error { // Do nothing for PENDING / DUPLICATE statuses return nil } + } else if payload.RpcGetIngestTxResponse.Status != "" { + channel = r.WebhookChannel } if channel == nil { return fmt.Errorf("payload could not be routed - channel is nil") diff --git a/internal/tss/router/router_test.go b/internal/tss/router/router_test.go index 58eea6f..e9d9660 100644 --- a/internal/tss/router/router_test.go +++ b/internal/tss/router/router_test.go @@ -109,7 +109,6 @@ func TestRouter(t *testing.T) { }, }, } - payload.RpcSubmitTxResponse.Code.TxResultCode = code webhookChannel. On("Send", payload). Return(). @@ -120,6 +119,24 @@ func TestRouter(t *testing.T) { webhookChannel.AssertCalled(t, "Send", payload) } }) + t.Run("get_ingest_resp_always_routes_to_webhook_cbannel", func(t *testing.T) { + payload := tss.Payload{ + RpcGetIngestTxResponse: tss.RPCGetIngestTxResponse{ + Status: entities.SuccessStatus, + Code: tss.RPCTXCode{ + TxResultCode: tss.FinalErrorCodes[0], + }, + }, + } + webhookChannel. + On("Send", payload). + Return(). + Once() + + _ = router.Route(payload) + + webhookChannel.AssertCalled(t, "Send", payload) + }) t.Run("nil_channel_does_not_route", func(t *testing.T) { payload := tss.Payload{} diff --git a/internal/tss/services/transaction_manager_test.go b/internal/tss/services/transaction_manager_test.go index 80731d4..f094ccb 100644 --- a/internal/tss/services/transaction_manager_test.go +++ b/internal/tss/services/transaction_manager_test.go @@ -9,7 +9,7 @@ import ( "github.com/stellar/wallet-backend/internal/db" "github.com/stellar/wallet-backend/internal/db/dbtest" "github.com/stellar/wallet-backend/internal/entities" - "github.com/stellar/wallet-backend/internal/services/servicesmocks" + "github.com/stellar/wallet-backend/internal/services" "github.com/stellar/wallet-backend/internal/tss" "github.com/stellar/wallet-backend/internal/tss/store" "github.com/stellar/wallet-backend/internal/tss/utils" @@ -26,7 +26,7 @@ func TestBuildAndSubmitTransaction(t *testing.T) { defer dbConnectionPool.Close() store := store.NewStore(dbConnectionPool) txServiceMock := TransactionServiceMock{} - rpcServiceMock := servicesmocks.RPCServiceMock{} + rpcServiceMock := services.RPCServiceMock{} txManager := NewTransactionManager(TransactionManagerConfigs{ TxService: &txServiceMock, RPCService: &rpcServiceMock, diff --git a/internal/tss/store/store.go b/internal/tss/store/store.go index b7953b0..18f8ea5 100644 --- a/internal/tss/store/store.go +++ b/internal/tss/store/store.go @@ -2,15 +2,21 @@ package store import ( "context" + "database/sql" + "errors" "fmt" + "time" "github.com/stellar/wallet-backend/internal/db" "github.com/stellar/wallet-backend/internal/tss" ) type Store interface { + GetTransaction(ctx context.Context, hash string) (Transaction, error) UpsertTransaction(ctx context.Context, WebhookURL string, txHash string, txXDR string, status tss.RPCTXStatus) error UpsertTry(ctx context.Context, transactionHash string, feeBumpTxHash string, feeBumpTxXDR string, status tss.RPCTXCode) error + GetTry(ctx context.Context, hash string) (Try, error) + GetTryByXDR(ctx context.Context, xdr string) (Try, error) } var _ Store = (*store)(nil) @@ -19,6 +25,24 @@ type store struct { DB db.ConnectionPool } +type Transaction struct { + Hash string `db:"transaction_hash"` + XDR string `db:"transaction_xdr"` + WebhookURL string `db:"webhook_url"` + Status string `db:"current_status"` + CreatedAt time.Time `db:"created_at"` + UpdatedAt time.Time `db:"updated_at"` + ClaimedUntil sql.NullTime `db:"claimed_until"` +} + +type Try struct { + Hash string `db:"try_transaction_hash"` + OrigTxHash string `db:"original_transaction_hash"` + XDR string `db:"try_transaction_xdr"` + Code int32 `db:"status"` + CreatedAt time.Time `db:"updated_at"` +} + func NewStore(db db.ConnectionPool) Store { return &store{ DB: db, @@ -64,3 +88,43 @@ func (s *store) UpsertTry(ctx context.Context, txHash string, feeBumpTxHash stri } return nil } + +func (s *store) GetTransaction(ctx context.Context, hash string) (Transaction, error) { + q := `SELECT * from tss_transactions where transaction_hash = $1` + var transaction Transaction + err := s.DB.GetContext(ctx, &transaction, q, hash) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + fmt.Println("empty") + return Transaction{}, nil + } + return Transaction{}, fmt.Errorf("getting transaction: %w", err) + } + return transaction, nil +} + +func (s *store) GetTry(ctx context.Context, hash string) (Try, error) { + q := `SELECT * from tss_transaction_submission_tries where try_transaction_hash = $1` + var try Try + err := s.DB.GetContext(ctx, &try, q, hash) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return Try{}, nil + } + return Try{}, fmt.Errorf("getting try: %w", err) + } + return try, nil +} + +func (s *store) GetTryByXDR(ctx context.Context, xdr string) (Try, error) { + q := `SELECT * from tss_transaction_submission_tries where try_transaction_xdr = $1` + var try Try + err := s.DB.GetContext(ctx, &try, q, xdr) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return Try{}, nil + } + return Try{}, fmt.Errorf("getting try: %w", err) + } + return try, nil +} diff --git a/internal/tss/store/store_test.go b/internal/tss/store/store_test.go index 2987a27..ef77f43 100644 --- a/internal/tss/store/store_test.go +++ b/internal/tss/store/store_test.go @@ -95,3 +95,69 @@ func TestUpsertTry(t *testing.T) { assert.Equal(t, numRows, 1) }) } + +func TestGetTransaction(t *testing.T) { + dbt := dbtest.Open(t) + defer dbt.Close() + dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) + require.NoError(t, err) + defer dbConnectionPool.Close() + store := NewStore(dbConnectionPool) + t.Run("transaction_exists", func(t *testing.T) { + status := tss.RPCTXStatus{OtherStatus: tss.NewStatus} + _ = store.UpsertTransaction(context.Background(), "localhost:8000", "hash", "xdr", status) + tx, err := store.GetTransaction(context.Background(), "hash") + assert.Equal(t, "xdr", tx.XDR) + assert.Empty(t, err) + + }) + t.Run("transaction_does_not_exist", func(t *testing.T) { + tx, _ := store.GetTransaction(context.Background(), "doesnotexist") + assert.Equal(t, Transaction{}, tx) + assert.Empty(t, err) + }) +} + +func TestGetTry(t *testing.T) { + dbt := dbtest.Open(t) + defer dbt.Close() + dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) + require.NoError(t, err) + defer dbConnectionPool.Close() + store := NewStore(dbConnectionPool) + t.Run("try_exists", func(t *testing.T) { + code := tss.RPCTXCode{OtherCodes: tss.NewCode} + _ = store.UpsertTry(context.Background(), "hash", "feebumptxhash", "feebumptxxdr", code) + try, err := store.GetTry(context.Background(), "feebumptxhash") + assert.Equal(t, try.OrigTxHash, "hash") + assert.Empty(t, err) + + }) + t.Run("try_does_not_exist", func(t *testing.T) { + try, _ := store.GetTry(context.Background(), "doesnotexist") + assert.Equal(t, Try{}, try) + assert.Empty(t, err) + }) +} + +func TestGetTryByXDR(t *testing.T) { + dbt := dbtest.Open(t) + defer dbt.Close() + dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) + require.NoError(t, err) + defer dbConnectionPool.Close() + store := NewStore(dbConnectionPool) + t.Run("try_exists", func(t *testing.T) { + code := tss.RPCTXCode{OtherCodes: tss.NewCode} + _ = store.UpsertTry(context.Background(), "hash", "feebumptxhash", "feebumptxxdr", code) + try, err := store.GetTryByXDR(context.Background(), "feebumptxxdr") + assert.Equal(t, try.OrigTxHash, "hash") + assert.Empty(t, err) + + }) + t.Run("try_does_not_exist", func(t *testing.T) { + try, _ := store.GetTryByXDR(context.Background(), "doesnotexist") + assert.Equal(t, Try{}, try) + assert.Empty(t, err) + }) +} diff --git a/internal/tss/types.go b/internal/tss/types.go index 6836a70..b10876d 100644 --- a/internal/tss/types.go +++ b/internal/tss/types.go @@ -25,7 +25,6 @@ type RPCGetIngestTxResponse struct { CreatedAt int64 } -//nolint:unused func ParseToRPCGetIngestTxResponse(result entities.RPCGetTransactionResult, err error) (RPCGetIngestTxResponse, error) { if err != nil { return RPCGetIngestTxResponse{Status: entities.ErrorStatus}, err @@ -42,7 +41,7 @@ func ParseToRPCGetIngestTxResponse(result entities.RPCGetTransactionResult, err return RPCGetIngestTxResponse{Status: entities.ErrorStatus}, fmt.Errorf("unable to parse createdAt: %w", err) } } - getIngestTxResponse.Code, err = parseSendTransactionErrorXDR(result.ResultXDR) + getIngestTxResponse.Code, err = TransactionResultXDRToCode(result.ResultXDR) if err != nil { return getIngestTxResponse, fmt.Errorf("parse error result xdr string: %w", err) } @@ -135,28 +134,36 @@ func ParseToRPCSendTxResponse(transactionXDR string, result entities.RPCSendTran } sendTxResponse.Status.RPCStatus = result.Status sendTxResponse.TransactionHash = result.Hash - sendTxResponse.Code, err = parseSendTransactionErrorXDR(result.ErrorResultXDR) + sendTxResponse.Code, err = TransactionResultXDRToCode(result.ErrorResultXDR) if err != nil { return sendTxResponse, fmt.Errorf("parse error result xdr string: %w", err) } return sendTxResponse, nil } -func parseSendTransactionErrorXDR(errorResultXDR string) (RPCTXCode, error) { +func UnmarshallTransactionResultXDR(resultXDR string) (xdr.TransactionResult, error) { + unmarshalErr := "unable to unmarshal errorResultXDR: %s" + decodedBytes, err := base64.StdEncoding.DecodeString(resultXDR) + if err != nil { + return xdr.TransactionResult{}, fmt.Errorf(unmarshalErr, resultXDR) + } + var txResultXDR xdr.TransactionResult + _, err = xdr3.Unmarshal(bytes.NewReader(decodedBytes), &txResultXDR) + if err != nil { + return xdr.TransactionResult{}, fmt.Errorf(unmarshalErr, resultXDR) + } + return txResultXDR, nil +} + +func TransactionResultXDRToCode(errorResultXDR string) (RPCTXCode, error) { if errorResultXDR == "" { return RPCTXCode{ OtherCodes: EmptyCode, }, nil } - unmarshalErr := "unable to unmarshal errorResultXDR: %s" - decodedBytes, err := base64.StdEncoding.DecodeString(errorResultXDR) - if err != nil { - return RPCTXCode{OtherCodes: UnmarshalBinaryCode}, fmt.Errorf(unmarshalErr, errorResultXDR) - } - var errorResult xdr.TransactionResult - _, err = xdr3.Unmarshal(bytes.NewReader(decodedBytes), &errorResult) + errorResult, err := UnmarshallTransactionResultXDR(errorResultXDR) if err != nil { - return RPCTXCode{OtherCodes: UnmarshalBinaryCode}, fmt.Errorf(unmarshalErr, errorResultXDR) + return RPCTXCode{OtherCodes: UnmarshalBinaryCode}, fmt.Errorf("unable to parse: %w", err) } return RPCTXCode{ TxResultCode: errorResult.Result.Code, diff --git a/internal/tss/types_test.go b/internal/tss/types_test.go index 10b67d1..ed489ff 100644 --- a/internal/tss/types_test.go +++ b/internal/tss/types_test.go @@ -36,7 +36,7 @@ func TestParseToRPCSendTxResponse(t *testing.T) { }, nil) assert.Equal(t, UnmarshalBinaryCode, resp.Code.OtherCodes) - assert.Equal(t, "parse error result xdr string: unable to unmarshal errorResultXDR: ABC123", err.Error()) + assert.Equal(t, "parse error result xdr string: unable to parse: unable to unmarshal errorResultXDR: ABC123", err.Error()) }) t.Run("response_has_errorResultXdr", func(t *testing.T) { diff --git a/internal/tss/utils/helpers.go b/internal/tss/utils/helpers.go index dec166c..94a7fad 100644 --- a/internal/tss/utils/helpers.go +++ b/internal/tss/utils/helpers.go @@ -18,6 +18,7 @@ func PayloadTOTSSResponse(payload tss.Payload) tss.TSSResponse { response.TransactionResultCode = payload.RpcGetIngestTxResponse.Code.TxResultCode.String() response.EnvelopeXDR = payload.RpcGetIngestTxResponse.EnvelopeXDR response.ResultXDR = payload.RpcGetIngestTxResponse.ResultXDR + response.CreatedAt = payload.RpcGetIngestTxResponse.CreatedAt } return response } From edcd0e740c13cd0a02cd6c9f8cce984e42d8ba1d Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Sun, 29 Sep 2024 20:09:23 -0700 Subject: [PATCH 080/113] delete file --- internal/tss/types_curr.go | 148 ------------------------------------- 1 file changed, 148 deletions(-) delete mode 100644 internal/tss/types_curr.go diff --git a/internal/tss/types_curr.go b/internal/tss/types_curr.go deleted file mode 100644 index cd8f17e..0000000 --- a/internal/tss/types_curr.go +++ /dev/null @@ -1,148 +0,0 @@ -package tss - -/* -import "github.com/stellar/go/xdr" - -type RPCTXStatus string -type OtherCodes int32 - -type TransactionResultCode int32 - -const ( - // Do not use NoCode - NoCode OtherCodes = 0 - // These values need to not overlap the values in xdr.TransactionResultCode - NewCode OtherCodes = 100 - RPCFailCode OtherCodes = 101 - UnMarshalBinaryCode OtherCodes = 102 -) - -type RPCTXCode struct { - TxResultCode xdr.TransactionResultCode - OtherCodes OtherCodes -} - -func (c RPCTXCode) Code() int { - if c.OtherCodes != NoCode { - return int(c.OtherCodes) - } - return int(c.TxResultCode) -} - -const ( - // Brand new transaction, not sent to RPC yet - NewStatus RPCTXStatus = "NEW" - // RPC sendTransaction statuses - PendingStatus RPCTXStatus = "PENDING" - DuplicateStatus RPCTXStatus = "DUPLICATE" - TryAgainLaterStatus RPCTXStatus = "TRY_AGAIN_LATER" - ErrorStatus RPCTXStatus = "ERROR" - // RPC getTransaction(s) statuses - NotFoundStatus RPCTXStatus = "NOT_FOUND" - FailedStatus RPCTXStatus = "FAILED" - SuccessStatus RPCTXStatus = "SUCCESS" -) - -var NonJitterErrorCodes = []xdr.TransactionResultCode{ - xdr.TransactionResultCodeTxTooEarly, - xdr.TransactionResultCodeTxTooLate, - xdr.TransactionResultCodeTxBadSeq, -} - -var JitterErrorCodes = []xdr.TransactionResultCode{ - xdr.TransactionResultCodeTxInsufficientFee, - xdr.TransactionResultCodeTxInternalError, -} - -type RPCGetIngestTxResponse struct { - // A status that indicated whether this transaction failed or successly made it to the ledger - Status RPCTXStatus - // The error code that is derived by deserialzing the ResultXdr string in the sendTransaction response - // list of possible errror codes: https://developers.stellar.org/docs/data/horizon/api-reference/errors/result-codes/transactions - Code RPCTXCode - // The raw TransactionEnvelope XDR for this transaction - EnvelopeXDR string - // The raw TransactionResult XDR of the envelopeXdr - ResultXDR string - Ledger int - ApplicationOrder int - // The unix timestamp of when the transaction was included in the ledger - CreatedAt int64 -} - -type RPCSendTxResponse struct { - // The hash of the transaction submitted to RPC - TransactionHash string - TransactionXDR string - // The status of an RPC sendTransaction call. Can be one of [PENDING, DUPLICATE, TRY_AGAIN_LATER, ERROR] - Status RPCTXStatus - // The (optional) error code that is derived by deserialzing the errorResultXdr string in the sendTransaction response - // list of possible errror codes: https://developers.stellar.org/docs/data/horizon/api-reference/errors/result-codes/transactions - Code RPCTXCode -} - -type Payload struct { - WebhookURL string - // The hash of the transaction xdr submitted by the client - the id of the transaction submitted by a client - TransactionHash string - // The xdr of the transaction - TransactionXDR string - // Relevant fields in an RPC sendTransaction response - RpcSubmitTxResponse RPCSendTxResponse - // Relevant fields in the transaction list inside the RPC getTransactions response - RpcGetIngestTxResponse RPCGetIngestTxResponse -} - -type Pagination struct { - Cursor string `json:"cursor,omitempty"` - Limit int `json:"limit"` -} - -type RPCParams struct { - Transaction string `json:"transaction,omitempty"` - Hash string `json:"hash,omitempty"` - StartLedger int `json:"startLedger,omitempty"` - Pagination Pagination `json:"pagination,omitempty"` -} - -type Transaction struct { - Status string `json:"status"` - ApplicationOrder int `json:"applicationOrder"` - FeeBump bool `json:"feeBump"` - EnvelopeXDR string `json:"envelopeXdr"` - ResultXDR string `json:"resultXdr"` - ResultMetaXDR string `json:"resultMetaXdr"` - Ledger int `json:"ledger"` - CreatedAt int `json:"createdAt"` -} - -type RPCResult struct { - Status string `json:"status"` - EnvelopeXDR string `json:"envelopeXdr"` - ResultXDR string `json:"resultXdr"` - ErrorResultXDR string `json:"errorResultXdr"` - Hash string `json:"hash"` - Transactions []Transaction `json:"transactions,omitempty"` - Cursor string `json:"cursor"` - CreatedAt string `json:"createdAt"` -} - -type RPCResponse struct { - RPCResult `json:"result"` -} - -type TSSResponse struct { - TransactionHash string `json:"tx_hash"` - TransactionResultCode string `json:"tx_result_code"` - Status string `json:"status"` - CreatedAt int64 `json:"created_at"` - EnvelopeXDR string `json:"envelopeXdr"` - ResultXDR string `json:"resultXdr"` -} - -type Channel interface { - Send(payload Payload) - Receive(payload Payload) - Stop() -} -*/ From cd321ae16964b13a9f85e9f005181f1fe2085e24 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Sun, 29 Sep 2024 20:25:26 -0700 Subject: [PATCH 081/113] fix lint errors --- internal/ingest/ingest.go | 2 +- internal/services/ingest.go | 39 +++++++++++++------------------ internal/services/ingest_test.go | 4 ++-- internal/utils/ingestion_utils.go | 4 +++- 4 files changed, 22 insertions(+), 27 deletions(-) diff --git a/internal/ingest/ingest.go b/internal/ingest/ingest.go index 2d8272a..dbf55bb 100644 --- a/internal/ingest/ingest.go +++ b/internal/ingest/ingest.go @@ -41,7 +41,7 @@ func Ingest(cfg Configs) error { } if err = ingestService.Run(ctx, uint32(cfg.StartLedger), uint32(cfg.EndLedger)); err != nil { - log.Ctx(ctx).Fatalf("Running ingest from %d to %d: %w", cfg.StartLedger, cfg.EndLedger) + log.Ctx(ctx).Fatalf("Running ingest from %d to %d: %v", cfg.StartLedger, cfg.EndLedger, err) } return nil diff --git a/internal/services/ingest.go b/internal/services/ingest.go index 6bffe5e..9d20a9f 100644 --- a/internal/services/ingest.go +++ b/internal/services/ingest.go @@ -6,7 +6,6 @@ import ( "fmt" "time" - "github.com/stellar/go/ingest" "github.com/stellar/go/support/log" "github.com/stellar/go/txnbuild" "github.com/stellar/go/xdr" @@ -172,9 +171,9 @@ func (m *ingestService) ingestPayments(ctx context.Context, ledgerTransactions [ case xdr.OperationTypePayment: fillPayment(&payment, op.Body) case xdr.OperationTypePathPaymentStrictSend: - fillPathSendRPC(&payment, op.Body, txResultXDR, opIdx) + fillPathSend(&payment, op.Body, txResultXDR, opIdx) case xdr.OperationTypePathPaymentStrictReceive: - fillPathReceiveRPC(&payment, op.Body, txResultXDR, opIdx) + fillPathReceive(&payment, op.Body, txResultXDR, opIdx) default: continue } @@ -207,8 +206,14 @@ func (m *ingestService) processTSSTransactions(ctx context.Context, ledgerTransa if err != nil { return fmt.Errorf("error unmarshaling resultxdr: %w", err) } - m.tssStore.UpsertTry(ctx, tssTry.OrigTxHash, tssTry.Hash, tssTry.XDR, code) - m.tssStore.UpsertTransaction(ctx, transaction.WebhookURL, tssTry.OrigTxHash, transaction.XDR, status) + err = m.tssStore.UpsertTry(ctx, tssTry.OrigTxHash, tssTry.Hash, tssTry.XDR, code) + if err != nil { + return fmt.Errorf("error updating try: %w", err) + } + err = m.tssStore.UpsertTransaction(ctx, transaction.WebhookURL, tssTry.OrigTxHash, transaction.XDR, status) + if err != nil { + return fmt.Errorf("error updating transaction: %w", err) + } txCode, err := tss.TransactionResultXDRToCode(tx.ResultXDR) if err != nil { @@ -267,23 +272,9 @@ func fillPayment(payment *data.Payment, operation xdr.OperationBody) { payment.DestAmount = payment.SrcAmount } -func fillPathSendRPC(payment *data.Payment, operation xdr.OperationBody, txResult xdr.TransactionResult, operationIdx int) { - pathOp := operation.MustPathPaymentStrictSendOp() - result := utils.OperationResultRPC(txResult, operationIdx).MustPathPaymentStrictSendResult() - payment.ToAddress = pathOp.Destination.Address() - payment.SrcAssetCode = utils.AssetCode(pathOp.SendAsset) - payment.SrcAssetIssuer = pathOp.SendAsset.GetIssuer() - payment.SrcAssetType = pathOp.SendAsset.Type.String() - payment.SrcAmount = int64(pathOp.SendAmount) - payment.DestAssetCode = utils.AssetCode(pathOp.DestAsset) - payment.DestAssetIssuer = pathOp.DestAsset.GetIssuer() - payment.DestAssetType = pathOp.DestAsset.Type.String() - payment.DestAmount = int64(result.DestAmount()) -} - -func fillPathSend(payment *data.Payment, operation xdr.OperationBody, transaction ingest.LedgerTransaction, operationIdx int) { +func fillPathSend(payment *data.Payment, operation xdr.OperationBody, txResult xdr.TransactionResult, operationIdx int) { pathOp := operation.MustPathPaymentStrictSendOp() - result := utils.OperationResult(transaction, operationIdx).MustPathPaymentStrictSendResult() + result := utils.OperationResult(txResult, operationIdx).MustPathPaymentStrictSendResult() payment.ToAddress = pathOp.Destination.Address() payment.SrcAssetCode = utils.AssetCode(pathOp.SendAsset) payment.SrcAssetIssuer = pathOp.SendAsset.GetIssuer() @@ -295,9 +286,9 @@ func fillPathSend(payment *data.Payment, operation xdr.OperationBody, transactio payment.DestAmount = int64(result.DestAmount()) } -func fillPathReceiveRPC(payment *data.Payment, operation xdr.OperationBody, txResult xdr.TransactionResult, operationIdx int) { +func fillPathReceive(payment *data.Payment, operation xdr.OperationBody, txResult xdr.TransactionResult, operationIdx int) { pathOp := operation.MustPathPaymentStrictReceiveOp() - result := utils.OperationResultRPC(txResult, operationIdx).MustPathPaymentStrictReceiveResult() + result := utils.OperationResult(txResult, operationIdx).MustPathPaymentStrictReceiveResult() payment.ToAddress = pathOp.Destination.Address() payment.SrcAssetCode = utils.AssetCode(pathOp.SendAsset) payment.SrcAssetIssuer = pathOp.SendAsset.GetIssuer() @@ -309,6 +300,7 @@ func fillPathReceiveRPC(payment *data.Payment, operation xdr.OperationBody, txRe payment.DestAmount = int64(pathOp.DestAmount) } +/* func fillPathReceive(payment *data.Payment, operation xdr.OperationBody, transaction ingest.LedgerTransaction, operationIdx int) { pathOp := operation.MustPathPaymentStrictReceiveOp() result := utils.OperationResult(transaction, operationIdx).MustPathPaymentStrictReceiveResult() @@ -322,3 +314,4 @@ func fillPathReceive(payment *data.Payment, operation xdr.OperationBody, transac payment.DestAssetType = pathOp.DestAsset.Type.String() payment.DestAmount = int64(pathOp.DestAmount) } +*/ diff --git a/internal/services/ingest_test.go b/internal/services/ingest_test.go index 11c2ae4..720c15e 100644 --- a/internal/services/ingest_test.go +++ b/internal/services/ingest_test.go @@ -141,8 +141,8 @@ func TestProcessTSSTransactions(t *testing.T) { }, } - tssStore.UpsertTransaction(context.Background(), "localhost:8000/webhook", "hash", "xdr", tss.RPCTXStatus{OtherStatus: tss.NewStatus}) - tssStore.UpsertTry(context.Background(), "hash", "feebumphash", "feebumpxdr", tss.RPCTXCode{OtherCodes: tss.NewCode}) + _ = tssStore.UpsertTransaction(context.Background(), "localhost:8000/webhook", "hash", "xdr", tss.RPCTXStatus{OtherStatus: tss.NewStatus}) + _ = tssStore.UpsertTry(context.Background(), "hash", "feebumphash", "feebumpxdr", tss.RPCTXCode{OtherCodes: tss.NewCode}) mockRouter. On("Route", mock.AnythingOfType("tss.Payload")). diff --git a/internal/utils/ingestion_utils.go b/internal/utils/ingestion_utils.go index 5a3fe20..0db333e 100644 --- a/internal/utils/ingestion_utils.go +++ b/internal/utils/ingestion_utils.go @@ -12,17 +12,19 @@ func OperationID(ledgerNumber, txNumber, opNumber int32) string { return toid.New(ledgerNumber, txNumber, opNumber).String() } -func OperationResultRPC(txResult xdr.TransactionResult, opNumber int) *xdr.OperationResultTr { +func OperationResult(txResult xdr.TransactionResult, opNumber int) *xdr.OperationResultTr { results, _ := txResult.OperationResults() tr := results[opNumber-1].MustTr() return &tr } +/* func OperationResult(tx ingest.LedgerTransaction, opNumber int) *xdr.OperationResultTr { results, _ := tx.Result.OperationResults() tr := results[opNumber-1].MustTr() return &tr } +*/ func TransactionID(ledgerNumber, txNumber int32) string { return toid.New(int32(ledgerNumber), int32(txNumber), 0).String() From 43711ca573938c8a2d1cc7aea358e293f32682b9 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Sun, 29 Sep 2024 20:31:38 -0700 Subject: [PATCH 082/113] remove dead code --- cmd/utils/custom_set_value.go | 44 ------------------------------- internal/utils/ingestion_utils.go | 12 --------- 2 files changed, 56 deletions(-) diff --git a/cmd/utils/custom_set_value.go b/cmd/utils/custom_set_value.go index 5c6e85d..d509e2b 100644 --- a/cmd/utils/custom_set_value.go +++ b/cmd/utils/custom_set_value.go @@ -2,9 +2,7 @@ package utils import ( "encoding/json" - "errors" "fmt" - "os" "strings" "github.com/sirupsen/logrus" @@ -83,48 +81,6 @@ func SetConfigOptionStellarPrivateKey(co *config.ConfigOption) error { return nil } -func SetConfigOptionCaptiveCoreBinPath(co *config.ConfigOption) error { - binPath := viper.GetString(co.Name) - - fileInfo, err := os.Stat(binPath) - if errors.Is(err, os.ErrNotExist) { - return fmt.Errorf("binary file %s does not exist", binPath) - } - - if fileInfo.IsDir() { - return fmt.Errorf("binary file path %s is a directory, not a file", binPath) - } - - key, ok := co.ConfigKey.(*string) - if !ok { - return unexpectedTypeError(key, co) - } - *key = binPath - - return nil -} - -func SetConfigOptionCaptiveCoreConfigDir(co *config.ConfigOption) error { - dirPath := viper.GetString(co.Name) - - fileInfo, err := os.Stat(dirPath) - if errors.Is(err, os.ErrNotExist) { - return fmt.Errorf("captive core configuration files dir %s does not exist", dirPath) - } - - if !fileInfo.IsDir() { - return fmt.Errorf("captive core configuration files dir %s is not a directory", dirPath) - } - - key, ok := co.ConfigKey.(*string) - if !ok { - return unexpectedTypeError(key, co) - } - *key = dirPath - - return nil -} - func SetConfigOptionAssets(co *config.ConfigOption) error { assetsJSON := viper.GetString(co.Name) diff --git a/internal/utils/ingestion_utils.go b/internal/utils/ingestion_utils.go index 0db333e..6fe0ead 100644 --- a/internal/utils/ingestion_utils.go +++ b/internal/utils/ingestion_utils.go @@ -18,22 +18,10 @@ func OperationResult(txResult xdr.TransactionResult, opNumber int) *xdr.Operatio return &tr } -/* -func OperationResult(tx ingest.LedgerTransaction, opNumber int) *xdr.OperationResultTr { - results, _ := tx.Result.OperationResults() - tr := results[opNumber-1].MustTr() - return &tr -} -*/ - func TransactionID(ledgerNumber, txNumber int32) string { return toid.New(int32(ledgerNumber), int32(txNumber), 0).String() } -func TransactionHash(ledgerMeta xdr.LedgerCloseMeta, txNumber int) string { - return ledgerMeta.TransactionHash(txNumber - 1).HexString() -} - // Memo returns the memo value parsed to string and its type. func Memo(memo xdr.Memo, txHash string) (*string, string) { memoType := memo.Type From 757e6d594036dcd35b811a6c67d17af6d64cc3e2 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Sun, 29 Sep 2024 20:42:31 -0700 Subject: [PATCH 083/113] fix broken test + delete unused code --- internal/services/ingest.go | 2 +- internal/tss/services/transaction_manager_test.go | 2 +- internal/utils/ingestion_utils.go | 14 ++------------ 3 files changed, 4 insertions(+), 14 deletions(-) diff --git a/internal/services/ingest.go b/internal/services/ingest.go index 9d20a9f..a432cf1 100644 --- a/internal/services/ingest.go +++ b/internal/services/ingest.go @@ -161,7 +161,7 @@ func (m *ingestService) ingestPayments(ctx context.Context, ledgerTransactions [ OperationType: op.Body.Type.String(), TransactionID: utils.TransactionID(int32(tx.Ledger), int32(tx.ApplicationOrder)), TransactionHash: tx.Hash, - FromAddress: utils.SourceAccountRPC(op, txEnvelopeXDR), + FromAddress: utils.SourceAccount(op, txEnvelopeXDR), CreatedAt: time.Unix(tx.CreatedAt, 0), Memo: txMemo, MemoType: txMemoType, diff --git a/internal/tss/services/transaction_manager_test.go b/internal/tss/services/transaction_manager_test.go index f094ccb..d8fe1d5 100644 --- a/internal/tss/services/transaction_manager_test.go +++ b/internal/tss/services/transaction_manager_test.go @@ -145,7 +145,7 @@ func TestBuildAndSubmitTransaction(t *testing.T) { _, err := txManager.BuildAndSubmitTransaction(context.Background(), "channel", payload) - assert.Equal(t, "channel: RPC fail: parse error result xdr string: unable to unmarshal errorResultXDR: ABCD", err.Error()) + assert.Equal(t, "channel: RPC fail: parse error result xdr string: unable to parse: unable to unmarshal errorResultXDR: ABCD", err.Error()) var txStatus string err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) diff --git a/internal/utils/ingestion_utils.go b/internal/utils/ingestion_utils.go index 6fe0ead..1235ce8 100644 --- a/internal/utils/ingestion_utils.go +++ b/internal/utils/ingestion_utils.go @@ -3,7 +3,6 @@ package utils import ( "strconv" - "github.com/stellar/go/ingest" "github.com/stellar/go/toid" "github.com/stellar/go/xdr" ) @@ -58,24 +57,15 @@ func Memo(memo xdr.Memo, txHash string) (*string, string) { return nil, memoType.String() } -func SourceAccountRPC(op xdr.Operation, txEnvelope xdr.TransactionEnvelope) string { +func SourceAccount(op xdr.Operation, txEnvelope xdr.TransactionEnvelope) string { account := op.SourceAccount if account != nil { return account.ToAccountId().Address() } - + txEnvelope.SourceAccount() return txEnvelope.SourceAccount().ToAccountId().Address() } -func SourceAccount(op xdr.Operation, tx ingest.LedgerTransaction) string { - account := op.SourceAccount - if account != nil { - return account.ToAccountId().Address() - } - - return tx.Envelope.SourceAccount().ToAccountId().Address() -} - func AssetCode(asset xdr.Asset) string { if asset.Type == xdr.AssetTypeAssetTypeNative { return "XLM" From 42edf1ffc09790078213ab43edf125d7a9ce5562 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Sun, 29 Sep 2024 20:46:06 -0700 Subject: [PATCH 084/113] tidy mod --- go.mod | 36 ----------------------- go.sum | 91 +--------------------------------------------------------- 2 files changed, 1 insertion(+), 126 deletions(-) diff --git a/go.mod b/go.mod index fb3f8f0..8328f2f 100644 --- a/go.mod +++ b/go.mod @@ -23,36 +23,20 @@ require ( ) require ( - cloud.google.com/go v0.112.0 // indirect - cloud.google.com/go/compute v1.23.3 // indirect - cloud.google.com/go/compute/metadata v0.2.3 // indirect - cloud.google.com/go/iam v1.1.5 // indirect - cloud.google.com/go/storage v1.37.0 // indirect github.com/BurntSushi/toml v1.3.2 // indirect github.com/Masterminds/squirrel v1.5.4 // indirect - github.com/Microsoft/go-winio v0.6.1 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/djherbis/fscache v0.10.1 // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/go-errors/errors v1.5.1 // indirect github.com/go-gorp/gorp/v3 v3.1.0 // indirect - github.com/go-logr/logr v1.3.0 // indirect - github.com/go-logr/stdr v1.2.2 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect - github.com/google/s2a-go v0.1.7 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect - github.com/googleapis/gax-go/v2 v2.12.0 // indirect github.com/gorilla/schema v1.2.0 // indirect - github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect @@ -63,7 +47,6 @@ require ( github.com/manucorporat/sse v0.0.0-20160126180136-ee05b128a739 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect @@ -81,31 +64,12 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect - go.opencensus.io v0.24.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 // indirect - go.opentelemetry.io/otel v1.21.0 // indirect - go.opentelemetry.io/otel/metric v1.21.0 // indirect - go.opentelemetry.io/otel/trace v1.21.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.21.0 // indirect - golang.org/x/mod v0.13.0 // indirect golang.org/x/net v0.23.0 // indirect - golang.org/x/oauth2 v0.16.0 // indirect - golang.org/x/sync v0.6.0 // indirect golang.org/x/sys v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect - golang.org/x/time v0.5.0 // indirect - golang.org/x/tools v0.14.0 // indirect - google.golang.org/api v0.157.0 // indirect - google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240122161410-6c6643bf1457 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac // indirect - google.golang.org/grpc v1.60.1 // indirect google.golang.org/protobuf v1.33.0 // indirect - gopkg.in/djherbis/atime.v1 v1.0.0 // indirect - gopkg.in/djherbis/stream.v1 v1.3.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/tylerb/graceful.v1 v1.2.15 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 65eb545..7147b9c 100644 --- a/go.sum +++ b/go.sum @@ -17,22 +17,14 @@ cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHOb cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= -cloud.google.com/go v0.112.0 h1:tpFCD7hpHFlQ8yPwT3x+QeXqc2T6+n6T+hmABHfDUSM= -cloud.google.com/go v0.112.0/go.mod h1:3jEEVwZ/MHU4djK5t5RHuKOA/GbLddgTdVubX1qnPD4= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk= -cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI= -cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= -cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/iam v1.1.5 h1:1jTsCu4bcsNsE4iiqNT5SHwrDRCfRmIaaaVFhRveTJI= -cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= @@ -43,8 +35,6 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= -cloud.google.com/go/storage v1.37.0 h1:WI8CsaFO8Q9KjPVtsZ5Cmi0dXV25zMoX0FklT7c3Jm4= -cloud.google.com/go/storage v1.37.0/go.mod h1:i34TiT2IhiNDmcj65PqwCjcoUX7Z5pLzS8DEmoiFq1k= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= @@ -52,8 +42,6 @@ github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbi github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= -github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= -github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU= github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= github.com/alitto/pond v1.9.2 h1:9Qb75z/scEZVCoSU+osVmQ0I0JOeLfdTDafrbcJ8CLs= @@ -66,8 +54,6 @@ github.com/aws/aws-sdk-go v1.45.26 h1:PJ2NJNY5N/yeobLYe1Y+xLdavBi67ZI8gvph6ftwVC github.com/aws/aws-sdk-go v1.45.26/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= -github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -78,27 +64,19 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4 h1:/inchEIKaYC1Akx+H+gqO04wryn5h75LSazbRlnya1k= -github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 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= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/djherbis/fscache v0.10.1 h1:hDv+RGyvD+UDKyRYuLoVNbuRTnf2SrA2K3VyR1br9lk= -github.com/djherbis/fscache v0.10.1/go.mod h1:yyPYtkNnnPXsW+81lAcQS6yab3G2CRfnPLotBvtbf0c= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/envoyproxy/protoc-gen-validate v1.0.2 h1:QkIBuU5k+x7/QXPvPPnWXWlCdaBFApVqftFV6k087DA= -github.com/envoyproxy/protoc-gen-validate v1.0.2/go.mod h1:GpiZQP3dDbg4JouG/NNS7QWXpgx6x8QiMKdmN72jogE= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= -github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= @@ -118,11 +96,6 @@ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2 github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= -github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -137,8 +110,6 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= @@ -161,7 +132,6 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= @@ -174,19 +144,15 @@ github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= -github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw= -github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= @@ -198,17 +164,11 @@ github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= -github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= -github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= -github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc= github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= @@ -216,8 +176,6 @@ github.com/guregu/null v4.0.0+incompatible h1:4zw0ckM7ECd6FNNddc3Fu4aty9nTlpkkzH github.com/guregu/null v4.0.0+incompatible/go.mod h1:ePGpQaN9cw0tj45IR5E5ehMvsFlLlQZAkkOXZurJ3NM= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= -github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= @@ -275,8 +233,6 @@ github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= -github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= -github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= @@ -343,7 +299,6 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= @@ -378,20 +333,6 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1 h1:SpGay3w+nEwMpfVnbqOLH5gY52/foP8RE8UzTZ1pdSE= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.46.1/go.mod h1:4UoMYEZOC0yN/sPGH76KPkkU7zgiEWYWL9vwmbnTJPE= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1 h1:aFJWCqJMNjENlcleuuOkGAPH82y0yULBScfXcIEdS24= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.46.1/go.mod h1:sEGXWArGqc3tVa+ekntsN65DmVbVeW+7lTKTjZF3/Fo= -go.opentelemetry.io/otel v1.21.0 h1:hzLeKBZEL7Okw2mGzZ0cc4k/A7Fta0uoPgaJCr8fsFc= -go.opentelemetry.io/otel v1.21.0/go.mod h1:QZzNPQPm1zLX4gZK4cMi+71eaorMSGT3A4znnUvNNEo= -go.opentelemetry.io/otel/metric v1.21.0 h1:tlYWfeo+Bocx5kLEloTjbcDwBuELRrIFxwdQ36PlJu4= -go.opentelemetry.io/otel/metric v1.21.0/go.mod h1:o1p3CA8nNHW8j5yuQLdc1eeqEaPfzug24uvsyIEJRWM= -go.opentelemetry.io/otel/sdk v1.21.0 h1:FTt8qirL1EysG6sTQRZ5TokkU8d0ugCj8htOgThZXQ8= -go.opentelemetry.io/otel/sdk v1.21.0/go.mod h1:Nna6Yv7PWTdgJHVRD9hIYywQBRx7pbox6nwBnZIxl/E= -go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8fpfLc= -go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= @@ -440,8 +381,6 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= -golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -470,7 +409,6 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= @@ -488,8 +426,6 @@ golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= -golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -501,8 +437,6 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= -golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -558,15 +492,12 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -615,14 +546,10 @@ golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= -golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= -golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -642,8 +569,6 @@ google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz513 google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/api v0.157.0 h1:ORAeqmbrrozeyw5NjnMxh7peHO0UzV4wWYSwZeCUb20= -google.golang.org/api v0.157.0/go.mod h1:+z4v4ufbZ1WEpld6yMGHyggs+PmAHiaLNj5ytP3N01g= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -651,8 +576,6 @@ google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= -google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -689,12 +612,6 @@ google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac h1:ZL/Teoy/ZGnzyrqK/Optxxp2pmVh+fmJ97slxSRyzUg= -google.golang.org/genproto v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:+Rvu7ElI+aLzyDQhpHMFMMltsD6m7nqpuWDd2CwJw3k= -google.golang.org/genproto/googleapis/api v0.0.0-20240122161410-6c6643bf1457 h1:KHBtwE+eQc3+NxpjmRFlQ3pJQ2FNnhhgB9xOV8kyBuU= -google.golang.org/genproto/googleapis/api v0.0.0-20240122161410-6c6643bf1457/go.mod h1:4jWUdICTdgc3Ibxmr8nAJiiLHwQBY0UI0XZcEMaFKaA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac h1:nUQEQmH/csSvFECKYRv6HWEyypysidKl2I6Qpsglq/0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240116215550-a9fa1716bcac/go.mod h1:daQN87bsDqDoe316QbbvX60nMoJQa4r6Ds0ZuoAe5yA= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -711,8 +628,6 @@ google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU= -google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -731,10 +646,6 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/djherbis/atime.v1 v1.0.0 h1:eMRqB/JrLKocla2PBPKgQYg/p5UG4L6AUAs92aP7F60= -gopkg.in/djherbis/atime.v1 v1.0.0/go.mod h1:hQIUStKmJfvf7xdh/wtK84qe+DsTV5LnA9lzxxtPpJ8= -gopkg.in/djherbis/stream.v1 v1.3.1 h1:uGfmsOY1qqMjQQphhRBSGLyA9qumJ56exkRu9ASTjCw= -gopkg.in/djherbis/stream.v1 v1.3.1/go.mod h1:aEV8CBVRmSpLamVJfM903Npic1IKmb2qS30VAZ+sssg= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/gavv/httpexpect.v1 v1.0.0-20170111145843-40724cf1e4a0 h1:r5ptJ1tBxVAeqw4CrYWhXIMr0SybY3CDHuIbCg5CFVw= gopkg.in/gavv/httpexpect.v1 v1.0.0-20170111145843-40724cf1e4a0/go.mod h1:WtiW9ZA1LdaWqtQRo1VbIL/v4XZ8NDta+O/kSpGgVek= @@ -760,4 +671,4 @@ honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= \ No newline at end of file +rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= From 5ff3984b617570fd55e78387f3b2bdf6a22c7cf7 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Sun, 29 Sep 2024 20:49:28 -0700 Subject: [PATCH 085/113] remove commented code --- cmd/utils/custom_set_value_test.go | 93 ------------------------------ 1 file changed, 93 deletions(-) diff --git a/cmd/utils/custom_set_value_test.go b/cmd/utils/custom_set_value_test.go index dfe7487..0e993a4 100644 --- a/cmd/utils/custom_set_value_test.go +++ b/cmd/utils/custom_set_value_test.go @@ -220,99 +220,6 @@ func Test_SetConfigOptionLogLevel(t *testing.T) { } } -/* -func TestSetConfigOptionCaptiveCoreBinPath(t *testing.T) { - opts := struct{ binPath string }{} - - co := config.ConfigOption{ - Name: "captive-core-bin-path", - OptType: types.String, - CustomSetValue: SetConfigOptionCaptiveCoreBinPath, - ConfigKey: &opts.binPath, - } - - testCases := []customSetterTestCase[string]{ - { - name: "returns an error if the file path is not set, should be caught by the Require() function", - wantErrContains: "binary file does not exist", - }, - { - name: "returns an error if the path is invalid", - args: []string{"--captive-core-bin-path", "/a/random/path/bin"}, - wantErrContains: "binary file /a/random/path/bin does not exist", - }, - { - name: "returns an error if the path format is invalid", - args: []string{"--captive-core-bin-path", "^7JcrS8J4q@V0$c"}, - wantErrContains: "binary file ^7JcrS8J4q@V0$c does not exist", - }, - { - name: "returns an error if the path is a directory, not a file", - args: []string{"--captive-core-bin-path", "./"}, - wantErrContains: "binary file path ./ is a directory, not a file", - }, - { - name: "sets to ENV var value", - envValue: "./custom_set_value_test.go", - wantResult: "./custom_set_value_test.go", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - opts.binPath = "" - customSetterTester(t, tc, co) - }) - } -} - -func TestSetConfigOptionCaptiveCoreConfigDir(t *testing.T) { - opts := struct{ binPath string }{} - - co := config.ConfigOption{ - Name: "captive-core-config-dir", - OptType: types.String, - CustomSetValue: SetConfigOptionCaptiveCoreConfigDir, - ConfigKey: &opts.binPath, - } - - testCases := []customSetterTestCase[string]{ - { - name: "returns an error if the file path is not set, should be caught by the Require() function", - wantErrContains: "captive core configuration files dir does not exist", - }, - { - name: "returns an error if the path is invalid", - envValue: "/a/random/path", - wantErrContains: "captive core configuration files dir /a/random/path does not exist", - }, - { - name: "returns an error if the path format is invalid", - envValue: "^7JcrS8J4q@V0$c", - wantErrContains: "captive core configuration files dir ^7JcrS8J4q@V0$c does not exist", - }, - - { - name: "returns an error if the path is a file, not a directory", - envValue: "./custom_set_value_test.go", - wantErrContains: "captive core configuration files dir ./custom_set_value_test.go is not a directory", - }, - { - name: "sets to ENV var value", - envValue: "../../internal/ingest/config", - wantResult: "../../internal/ingest/config", - }, - } - - for _, tc := range testCases { - t.Run(tc.name, func(t *testing.T) { - opts.binPath = "" - customSetterTester(t, tc, co) - }) - } -} -*/ - func TestSetConfigOptionAssets(t *testing.T) { opts := struct{ assets []entities.Asset }{} From 715ae3f95ba04f091b88bdc543c641588e094f2f Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Sun, 29 Sep 2024 21:51:49 -0700 Subject: [PATCH 086/113] delete file --- .../tss/utils/transaction_service_test.go | 571 ------------------ 1 file changed, 571 deletions(-) delete mode 100644 internal/tss/utils/transaction_service_test.go diff --git a/internal/tss/utils/transaction_service_test.go b/internal/tss/utils/transaction_service_test.go deleted file mode 100644 index 93eee35..0000000 --- a/internal/tss/utils/transaction_service_test.go +++ /dev/null @@ -1,571 +0,0 @@ -package utils - -/* - -import ( - "bytes" - "context" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "strings" - "testing" - - "github.com/stellar/go/clients/horizonclient" - "github.com/stellar/go/keypair" - "github.com/stellar/go/protocols/horizon" - "github.com/stellar/go/txnbuild" - "github.com/stellar/go/xdr" - "github.com/stellar/wallet-backend/internal/signing" - "github.com/stellar/wallet-backend/internal/tss" - tsserror "github.com/stellar/wallet-backend/internal/tss/errors" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" -) - -func TestSignAndBuildNewFeeBumpTransaction(t *testing.T) { - distributionAccountSignatureClient := signing.SignatureClientMock{} - defer distributionAccountSignatureClient.AssertExpectations(t) - channelAccountSignatureClient := signing.SignatureClientMock{} - defer channelAccountSignatureClient.AssertExpectations(t) - horizonClient := horizonclient.MockClient{} - defer horizonClient.AssertExpectations(t) - txService, _ := NewTransactionService(TransactionServiceOptions{ - DistributionAccountSignatureClient: &distributionAccountSignatureClient, - ChannelAccountSignatureClient: &channelAccountSignatureClient, - HorizonClient: &horizonClient, - RPCURL: "http://localhost:8000/soroban/rpc", - BaseFee: 114, - HTTPClient: &MockHTTPClient{}, - }) - - txStr, _ := BuildTestTransaction().Base64() - - t.Run("malformed_transaction_string", func(t *testing.T) { - feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), "abcd") - assert.Empty(t, feeBumpTx) - assert.ErrorIs(t, tsserror.OriginalXDRMalformed, err) - }) - - t.Run("channel_account_signature_client_get_account_public_key_err", func(t *testing.T) { - channelAccountSignatureClient. - On("GetAccountPublicKey", context.Background()). - Return("", errors.New("channel accounts unavailable")). - Once() - - feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) - assert.Empty(t, feeBumpTx) - assert.Equal(t, "getting channel account public key: channel accounts unavailable", err.Error()) - }) - - t.Run("horizon_client_get_account_detail_err", func(t *testing.T) { - channelAccount := keypair.MustRandom() - channelAccountSignatureClient. - On("GetAccountPublicKey", context.Background()). - Return(channelAccount.Address(), nil). - Once() - - horizonClient. - On("AccountDetail", horizonclient.AccountRequest{ - AccountID: channelAccount.Address(), - }). - Return(horizon.Account{}, errors.New("horizon down")). - Once() - - feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) - assert.Empty(t, feeBumpTx) - assert.Equal(t, "getting channel account details from horizon: horizon down", err.Error()) - }) - - t.Run("horizon_client_sign_stellar_transaction_w_channel_account_err", func(t *testing.T) { - channelAccount := keypair.MustRandom() - channelAccountSignatureClient. - On("GetAccountPublicKey", context.Background()). - Return(channelAccount.Address(), nil). - Once(). - On("SignStellarTransaction", context.Background(), mock.AnythingOfType("*txnbuild.Transaction"), []string{channelAccount.Address()}). - Return(nil, errors.New("unable to sign")). - Once() - - horizonClient. - On("AccountDetail", horizonclient.AccountRequest{ - AccountID: channelAccount.Address(), - }). - Return(horizon.Account{AccountID: channelAccount.Address(), Sequence: 1}, nil). - Once() - - feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) - assert.Empty(t, feeBumpTx) - assert.Equal(t, "signing transaction with channel account: unable to sign", err.Error()) - }) - - t.Run("distribution_account_signature_client_get_account_public_key_err", func(t *testing.T) { - channelAccount := keypair.MustRandom() - signedTx := txnbuild.Transaction{} - channelAccountSignatureClient. - On("GetAccountPublicKey", context.Background()). - Return(channelAccount.Address(), nil). - Once(). - On("SignStellarTransaction", context.Background(), mock.AnythingOfType("*txnbuild.Transaction"), []string{channelAccount.Address()}). - Return(&signedTx, nil). - Once() - - distributionAccountSignatureClient. - On("GetAccountPublicKey", context.Background()). - Return("", errors.New("client down")). - Once() - - horizonClient. - On("AccountDetail", horizonclient.AccountRequest{ - AccountID: channelAccount.Address(), - }). - Return(horizon.Account{AccountID: channelAccount.Address(), Sequence: 1}, nil). - Once() - - feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) - assert.Empty(t, feeBumpTx) - assert.Equal(t, "getting distribution account public key: client down", err.Error()) - }) - - t.Run("horizon_client_sign_stellar_transaction_w_distribition_account_err", func(t *testing.T) { - account := keypair.MustRandom() - signedTx := BuildTestTransaction() - channelAccountSignatureClient. - On("GetAccountPublicKey", context.Background()). - Return(account.Address(), nil). - Once(). - On("SignStellarTransaction", context.Background(), mock.AnythingOfType("*txnbuild.Transaction"), []string{account.Address()}). - Return(signedTx, nil). - Once() - - distributionAccountSignatureClient. - On("GetAccountPublicKey", context.Background()). - Return(account.Address(), nil). - Once(). - On("SignStellarFeeBumpTransaction", context.Background(), mock.AnythingOfType("*txnbuild.FeeBumpTransaction")). - Return(nil, errors.New("unable to sign")). - Once() - - horizonClient. - On("AccountDetail", horizonclient.AccountRequest{ - AccountID: account.Address(), - }). - Return(horizon.Account{AccountID: account.Address(), Sequence: 1}, nil). - Once() - - feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) - assert.Empty(t, feeBumpTx) - assert.Equal(t, "signing the fee bump transaction with distribution account: unable to sign", err.Error()) - }) - - t.Run("returns_signed_tx", func(t *testing.T) { - account := keypair.MustRandom() - signedTx := BuildTestTransaction() - testFeeBumpTx, _ := txnbuild.NewFeeBumpTransaction( - txnbuild.FeeBumpTransactionParams{ - Inner: signedTx, - FeeAccount: account.Address(), - BaseFee: int64(100), - }, - ) - channelAccountSignatureClient. - On("GetAccountPublicKey", context.Background()). - Return(account.Address(), nil). - Once(). - On("SignStellarTransaction", context.Background(), mock.AnythingOfType("*txnbuild.Transaction"), []string{account.Address()}). - Return(signedTx, nil). - Once() - - distributionAccountSignatureClient. - On("GetAccountPublicKey", context.Background()). - Return(account.Address(), nil). - Once(). - On("SignStellarFeeBumpTransaction", context.Background(), mock.AnythingOfType("*txnbuild.FeeBumpTransaction")). - Return(testFeeBumpTx, nil). - Once() - - horizonClient. - On("AccountDetail", horizonclient.AccountRequest{ - AccountID: account.Address(), - }). - Return(horizon.Account{AccountID: account.Address(), Sequence: 1}, nil). - Once() - - feeBumpTx, err := txService.SignAndBuildNewFeeBumpTransaction(context.Background(), txStr) - assert.Equal(t, feeBumpTx, testFeeBumpTx) - assert.Empty(t, err) - }) -} - -func TestParseErrorResultXDR(t *testing.T) { - distributionAccountSignatureClient := signing.SignatureClientMock{} - defer distributionAccountSignatureClient.AssertExpectations(t) - channelAccountSignatureClient := signing.SignatureClientMock{} - defer channelAccountSignatureClient.AssertExpectations(t) - horizonClient := horizonclient.MockClient{} - defer horizonClient.AssertExpectations(t) - txService, _ := NewTransactionService(TransactionServiceOptions{ - DistributionAccountSignatureClient: &distributionAccountSignatureClient, - ChannelAccountSignatureClient: &channelAccountSignatureClient, - HorizonClient: &horizonClient, - RPCURL: "http://localhost:8000/soroban/rpc", - BaseFee: 114, - HTTPClient: &MockHTTPClient{}, - }) - - t.Run("errorResultXdr_empty", func(t *testing.T) { - _, err := txService.parseErrorResultXDR("") - assert.Equal(t, "unable to unmarshal errorResultXdr: ", err.Error()) - }) - - t.Run("errorResultXdr_invalid", func(t *testing.T) { - _, err := txService.parseErrorResultXDR("ABCD") - assert.Equal(t, "unable to unmarshal errorResultXdr: ABCD", err.Error()) - }) - - t.Run("errorResultXdr_valid", func(t *testing.T) { - resp, err := txService.parseErrorResultXDR("AAAAAAAAAMj////9AAAAAA==") - assert.Equal(t, xdr.TransactionResultCodeTxTooLate, resp.TxResultCode) - assert.Empty(t, err) - }) -} - -type errorReader struct{} - -func (e *errorReader) Read(p []byte) (n int, err error) { - return 0, fmt.Errorf("read error") -} - -func (e *errorReader) Close() error { - return nil -} - -func TestSendRPCRequest(t *testing.T) { - mockHTTPClient := MockHTTPClient{} - rpcURL := "http://localhost:8000/soroban/rpc" - txService, _ := NewTransactionService(TransactionServiceOptions{ - DistributionAccountSignatureClient: &signing.SignatureClientMock{}, - ChannelAccountSignatureClient: &signing.SignatureClientMock{}, - HorizonClient: &horizonclient.MockClient{}, - RPCURL: rpcURL, - BaseFee: 114, - HTTPClient: &mockHTTPClient, - }) - method := "sendTransaction" - params := tss.RPCParams{Transaction: "ABCD"} - payload := map[string]interface{}{ - "jsonrpc": "2.0", - "id": 1, - "method": method, - "params": params, - } - jsonData, _ := json.Marshal(payload) - t.Run("rpc_post_call_fails", func(t *testing.T) { - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(&http.Response{}, errors.New("RPC Connection fail")). - Once() - - resp, err := txService.sendRPCRequest(method, params) - - assert.Empty(t, resp) - assert.Equal(t, "sendTransaction: sending POST request to rpc: RPC Connection fail", err.Error()) - }) - - t.Run("unmarshaling_rpc_response_fails", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(&errorReader{}), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.sendRPCRequest(method, params) - - assert.Empty(t, resp) - assert.Equal(t, "sendTransaction: unmarshaling RPC response", err.Error()) - }) - - t.Run("unmarshaling_json_fails", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{invalid-json`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.sendRPCRequest(method, params) - - assert.Empty(t, resp) - assert.Equal(t, "sendTransaction: parsing RPC response JSON", err.Error()) - }) - - t.Run("response_has_no_result_field", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"status": "success"}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, _ := txService.sendRPCRequest(method, params) - assert.Empty(t, resp) - }) - - t.Run("response_has_status_field", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"status": "PENDING"}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.sendRPCRequest(method, params) - - assert.Equal(t, "PENDING", resp.Status) - assert.Empty(t, err) - }) - - t.Run("response_has_envelopexdr_field", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"envelopeXdr": "exdr"}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.sendRPCRequest(method, params) - - assert.Equal(t, "exdr", resp.EnvelopeXDR) - assert.Empty(t, err) - }) - - t.Run("response_has_resultxdr_field", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"resultXdr": "rxdr"}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.sendRPCRequest(method, params) - - assert.Equal(t, "rxdr", resp.ResultXDR) - assert.Empty(t, err) - }) - - t.Run("response_has_errorresultxdr_field", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"errorResultXdr": "exdr"}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.sendRPCRequest(method, params) - - assert.Equal(t, "exdr", resp.ErrorResultXDR) - assert.Empty(t, err) - }) - - t.Run("response_has_hash_field", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"hash": "hash"}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.sendRPCRequest(method, params) - - assert.Equal(t, "hash", resp.Hash) - assert.Empty(t, err) - }) - - t.Run("response_has_createdat_field", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"createdAt": "1234"}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.sendRPCRequest(method, params) - - assert.Equal(t, "1234", resp.CreatedAt) - assert.Empty(t, err) - }) -} - -func TestSendTransaction(t *testing.T) { - mockHTTPClient := MockHTTPClient{} - rpcURL := "http://localhost:8000/soroban/rpc" - txService, _ := NewTransactionService(TransactionServiceOptions{ - DistributionAccountSignatureClient: &signing.SignatureClientMock{}, - ChannelAccountSignatureClient: &signing.SignatureClientMock{}, - HorizonClient: &horizonclient.MockClient{}, - RPCURL: rpcURL, - BaseFee: 114, - HTTPClient: &mockHTTPClient, - }) - method := "sendTransaction" - params := tss.RPCParams{Transaction: "ABCD"} - payload := map[string]interface{}{ - "jsonrpc": "2.0", - "id": 1, - "method": method, - "params": params, - } - jsonData, _ := json.Marshal(payload) - - t.Run("rpc_request_fails", func(t *testing.T) { - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(&http.Response{}, errors.New("RPC Connection fail")). - Once() - - resp, err := txService.SendTransaction("ABCD") - - assert.Equal(t, tss.ErrorStatus, resp.Status) - assert.Equal(t, tss.RPCFailCode, resp.Code.OtherCodes) - assert.Equal(t, "RPC fail: sendTransaction: sending POST request to rpc: RPC Connection fail", err.Error()) - - }) - t.Run("response_has_unparsable_errorResultXdr", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"status": "ERROR", "errorResultXdr": "ABC123"}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.SendTransaction("ABCD") - - assert.Equal(t, tss.ErrorStatus, resp.Status) - assert.Equal(t, tss.UnMarshalBinaryCode, resp.Code.OtherCodes) - assert.Equal(t, "unable to unmarshal errorResultXdr: ABC123", err.Error()) - }) - t.Run("response_has_empty_errorResultXdr_wth_status", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"status": "PENDING", "errorResultXdr": ""}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.SendTransaction("ABCD") - - assert.Equal(t, tss.PendingStatus, resp.Status) - assert.Equal(t, tss.UnMarshalBinaryCode, resp.Code.OtherCodes) - assert.Equal(t, "unable to unmarshal errorResultXdr: ", err.Error()) - }) - t.Run("response_has_errorResultXdr", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"status": "ERROR", "errorResultXdr": "AAAAAAAAAMj////9AAAAAA=="}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.SendTransaction("ABCD") - - assert.Equal(t, tss.ErrorStatus, resp.Status) - assert.Equal(t, xdr.TransactionResultCodeTxTooLate, resp.Code.TxResultCode) - assert.Empty(t, err) - }) -} - -func TestGetTransaction(t *testing.T) { - mockHTTPClient := MockHTTPClient{} - rpcURL := "http://localhost:8000/soroban/rpc" - txService, _ := NewTransactionService(TransactionServiceOptions{ - DistributionAccountSignatureClient: &signing.SignatureClientMock{}, - ChannelAccountSignatureClient: &signing.SignatureClientMock{}, - HorizonClient: &horizonclient.MockClient{}, - RPCURL: rpcURL, - BaseFee: 114, - HTTPClient: &mockHTTPClient, - }) - method := "getTransaction" - params := tss.RPCParams{Hash: "XYZ"} - payload := map[string]interface{}{ - "jsonrpc": "2.0", - "id": 1, - "method": method, - "params": params, - } - jsonData, _ := json.Marshal(payload) - - t.Run("rpc_request_fails", func(t *testing.T) { - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(&http.Response{}, errors.New("RPC Connection fail")). - Once() - - resp, err := txService.GetTransaction("XYZ") - - assert.Equal(t, tss.ErrorStatus, resp.Status) - assert.Equal(t, "RPC Fail: getTransaction: sending POST request to rpc: RPC Connection fail", err.Error()) - - }) - t.Run("unable_to_parse_createdAt", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"status": "SUCCESS", "createdAt": "ABCD"}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.GetTransaction("XYZ") - - assert.Equal(t, tss.ErrorStatus, resp.Status) - assert.Equal(t, "unable to parse createAt: strconv.ParseInt: parsing \"ABCD\": invalid syntax", err.Error()) - }) - t.Run("response_has_createdAt_resultXdr_field", func(t *testing.T) { - httpResponse := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader(`{"result": {"status": "FAILED", "resultXdr": "AAAAAAAAAMj////9AAAAAA==", "createdAt": "1234567"}}`)), - } - mockHTTPClient. - On("Post", rpcURL, "application/json", bytes.NewBuffer(jsonData)). - Return(httpResponse, nil). - Once() - - resp, err := txService.GetTransaction("XYZ") - - assert.Equal(t, tss.FailedStatus, resp.Status) - assert.Equal(t, xdr.TransactionResultCodeTxTooLate, resp.Code.TxResultCode) - assert.Equal(t, int64(1234567), resp.CreatedAt) - assert.Empty(t, err) - }) - -} -*/ From 6b2a6f7e4832d7fce72351b86a6172b276ef7d2c Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Mon, 30 Sep 2024 15:42:18 -0700 Subject: [PATCH 087/113] changes based on comments --- internal/entities/rpc.go | 10 ---------- internal/serve/serve.go | 17 ++++++++++------- .../servicesmocks/rpc_service_mocks.go | 4 ---- internal/tss/channels/rpc_caller_channel.go | 13 +++++-------- .../tss/channels/rpc_caller_channel_test.go | 4 ++-- internal/tss/services/transaction_manager.go | 2 +- .../tss/services/transaction_manager_test.go | 2 +- internal/tss/store/store.go | 19 +++++++++++-------- internal/tss/store/store_test.go | 4 ++-- 9 files changed, 32 insertions(+), 43 deletions(-) diff --git a/internal/entities/rpc.go b/internal/entities/rpc.go index cec0ecc..552ba49 100644 --- a/internal/entities/rpc.go +++ b/internal/entities/rpc.go @@ -18,22 +18,12 @@ const ( SuccessStatus RPCStatus = "SUCCESS" ) -type RPCEntry struct { - Key string `json:"key"` - XDR string `json:"xdr"` - LastModifiedLedgerSeq int64 `json:"lastModifiedLedgerSeq"` -} - type RPCResponse struct { Result json.RawMessage `json:"result"` JSONRPC string `json:"jsonrpc"` ID int64 `json:"id"` } -type RPCGetLedgerEntriesResult struct { - Entries []RPCEntry `json:"entries"` -} - type RPCGetTransactionResult struct { Status RPCStatus `json:"status"` LatestLedger int64 `json:"latestLedger"` diff --git a/internal/serve/serve.go b/internal/serve/serve.go index c960bd5..00ee74f 100644 --- a/internal/serve/serve.go +++ b/internal/serve/serve.go @@ -85,7 +85,8 @@ type handlerDeps struct { // TSS RPCCallerServiceChannel tss.Channel TSSRouter tssrouter.Router - AppTracker apptracker.AppTracker + // Error Tracker + AppTracker apptracker.AppTracker } func Serve(cfg Configs) error { @@ -172,7 +173,7 @@ func initHandlerDeps(cfg Configs) (handlerDeps, error) { DistributionAccountSignatureClient: cfg.DistributionAccountSignatureClient, ChannelAccountSignatureClient: cfg.ChannelAccountSignatureClient, HorizonClient: &horizonClient, - BaseFee: int64(cfg.BaseFee), // Reuse horizon base fee for RPC?? + BaseFee: int64(cfg.BaseFee), } tssTxService, err := tssservices.NewTransactionService(txServiceOpts) if err != nil { @@ -184,20 +185,22 @@ func initHandlerDeps(cfg Configs) (handlerDeps, error) { return handlerDeps{}, fmt.Errorf("instantiating rpc service: %w", err) } - // re-use same context as above?? - store := tssstore.NewStore(dbConnectionPool) + store, err := tssstore.NewStore(dbConnectionPool) + if err != nil { + return handlerDeps{}, fmt.Errorf("instantiating tss store: %w", err) + } txManager := tssservices.NewTransactionManager(tssservices.TransactionManagerConfigs{ TxService: tssTxService, RPCService: rpcService, Store: store, }) - tssChannelConfigs := tsschannel.RPCCallerChannelConfigs{ + + rpcCallerServiceChannel := tsschannel.NewRPCCallerChannel(tsschannel.RPCCallerChannelConfigs{ TxManager: txManager, Store: store, MaxBufferSize: cfg.RPCCallerServiceChannelBufferSize, MaxWorkers: cfg.RPCCallerServiceChannelMaxWorkers, - } - rpcCallerServiceChannel := tsschannel.NewRPCCallerChannel(tssChannelConfigs) + }) router := tssrouter.NewRouter(tssrouter.RouterConfigs{ RPCCallerChannel: rpcCallerServiceChannel, diff --git a/internal/services/servicesmocks/rpc_service_mocks.go b/internal/services/servicesmocks/rpc_service_mocks.go index f72c348..7c6053d 100644 --- a/internal/services/servicesmocks/rpc_service_mocks.go +++ b/internal/services/servicesmocks/rpc_service_mocks.go @@ -21,7 +21,3 @@ func (r *RPCServiceMock) GetTransaction(transactionHash string) (entities.RPCGet args := r.Called(transactionHash) return args.Get(0).(entities.RPCGetTransactionResult), args.Error(1) } - -type TransactionManagerMock struct { - mock.Mock -} diff --git a/internal/tss/channels/rpc_caller_channel.go b/internal/tss/channels/rpc_caller_channel.go index 9a36318..4e8b276 100644 --- a/internal/tss/channels/rpc_caller_channel.go +++ b/internal/tss/channels/rpc_caller_channel.go @@ -6,7 +6,6 @@ import ( "github.com/alitto/pond" "github.com/stellar/go/support/log" - "github.com/stellar/wallet-backend/internal/entities" "github.com/stellar/wallet-backend/internal/tss" "github.com/stellar/wallet-backend/internal/tss/router" "github.com/stellar/wallet-backend/internal/tss/services" @@ -54,21 +53,19 @@ func (p *rpcCallerPool) Receive(payload tss.Payload) { err := p.Store.UpsertTransaction(ctx, payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.RPCTXStatus{OtherStatus: tss.NewStatus}) if err != nil { - log.Errorf("%s: Unable to upsert transaction into transactions table: %e", RPCCallerChannelName, err) + log.Errorf("%s: unable to upsert transaction into transactions table: %e", RPCCallerChannelName, err) return } rpcSendResp, err := p.TxManager.BuildAndSubmitTransaction(ctx, RPCCallerChannelName, payload) if err != nil { - log.Errorf("%s: Unable to sign and submit transaction: %e", RPCCallerChannelName, err) + log.Errorf("%s: unable to sign and submit transaction: %e", RPCCallerChannelName, err) return } payload.RpcSubmitTxResponse = rpcSendResp - if rpcSendResp.Status.RPCStatus == entities.TryAgainLaterStatus || rpcSendResp.Status.RPCStatus == entities.ErrorStatus { - err = p.Router.Route(payload) - if err != nil { - log.Errorf("%s: Unable to route payload: %e", RPCCallerChannelName, err) - } + err = p.Router.Route(payload) + if err != nil { + log.Errorf("%s: unable to route payload: %e", RPCCallerChannelName, err) } } diff --git a/internal/tss/channels/rpc_caller_channel_test.go b/internal/tss/channels/rpc_caller_channel_test.go index b5899c0..a46d063 100644 --- a/internal/tss/channels/rpc_caller_channel_test.go +++ b/internal/tss/channels/rpc_caller_channel_test.go @@ -21,7 +21,7 @@ func TestSend(t *testing.T) { dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) require.NoError(t, err) defer dbConnectionPool.Close() - store := store.NewStore(dbConnectionPool) + store, _ := store.NewStore(dbConnectionPool) txManagerMock := services.TransactionManagerMock{} routerMock := router.MockRouter{} cfgs := RPCCallerChannelConfigs{ @@ -64,7 +64,7 @@ func TestReceivee(t *testing.T) { dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) require.NoError(t, err) defer dbConnectionPool.Close() - store := store.NewStore(dbConnectionPool) + store, _ := store.NewStore(dbConnectionPool) txManagerMock := services.TransactionManagerMock{} routerMock := router.MockRouter{} cfgs := RPCCallerChannelConfigs{ diff --git a/internal/tss/services/transaction_manager.go b/internal/tss/services/transaction_manager.go index 7825582..5fa3c63 100644 --- a/internal/tss/services/transaction_manager.go +++ b/internal/tss/services/transaction_manager.go @@ -64,7 +64,7 @@ func (t *transactionManager) BuildAndSubmitTransaction(ctx context.Context, chan return rpcSendResp, fmt.Errorf("%s: RPC fail: %w", channelName, parseErr) } - if rpcErr != nil && rpcSendResp.Code.OtherCodes == tss.RPCFailCode || rpcSendResp.Code.OtherCodes == tss.UnmarshalBinaryCode { + if parseErr != nil && rpcSendResp.Code.OtherCodes == tss.RPCFailCode || rpcSendResp.Code.OtherCodes == tss.UnmarshalBinaryCode { return tss.RPCSendTxResponse{}, fmt.Errorf("%s: RPC fail: %w", channelName, rpcErr) } diff --git a/internal/tss/services/transaction_manager_test.go b/internal/tss/services/transaction_manager_test.go index 80731d4..70c1df1 100644 --- a/internal/tss/services/transaction_manager_test.go +++ b/internal/tss/services/transaction_manager_test.go @@ -24,7 +24,7 @@ func TestBuildAndSubmitTransaction(t *testing.T) { dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) require.NoError(t, err) defer dbConnectionPool.Close() - store := store.NewStore(dbConnectionPool) + store, _ := store.NewStore(dbConnectionPool) txServiceMock := TransactionServiceMock{} rpcServiceMock := servicesmocks.RPCServiceMock{} txManager := NewTransactionManager(TransactionManagerConfigs{ diff --git a/internal/tss/store/store.go b/internal/tss/store/store.go index b7953b0..7d5bc81 100644 --- a/internal/tss/store/store.go +++ b/internal/tss/store/store.go @@ -19,10 +19,13 @@ type store struct { DB db.ConnectionPool } -func NewStore(db db.ConnectionPool) Store { +func NewStore(db db.ConnectionPool) (Store, error) { + if db == nil { + return nil, fmt.Errorf("db cannot be nil") + } return &store{ DB: db, - } + }, nil } func (s *store) UpsertTransaction(ctx context.Context, webhookURL string, txHash string, txXDR string, status tss.RPCTXStatus) error { @@ -33,9 +36,9 @@ func (s *store) UpsertTransaction(ctx context.Context, webhookURL string, txHash ($1, $2, $3, $4) ON CONFLICT (transaction_hash) DO UPDATE SET - transaction_xdr = $2, - webhook_url = $3, - current_status = $4, + transaction_xdr = EXCLUDED.transaction_xdr, + webhook_url = EXCLUDED.webhook_url, + current_status = EXCLUDED.current_status, updated_at = NOW(); ` _, err := s.DB.ExecContext(ctx, q, txHash, txXDR, webhookURL, status.Status()) @@ -53,9 +56,9 @@ func (s *store) UpsertTry(ctx context.Context, txHash string, feeBumpTxHash stri ($1, $2, $3, $4) ON CONFLICT (try_transaction_hash) DO UPDATE SET - original_transaction_hash = $1, - try_transaction_xdr = $3, - status = $4, + original_transaction_hash = EXCLUDED.original_transaction_hash, + try_transaction_xdr = EXCLUDED.try_transaction_xdr, + status = EXCLUDED.status, updated_at = NOW(); ` _, err := s.DB.ExecContext(ctx, q, txHash, feeBumpTxHash, feeBumpTxXDR, status.Code()) diff --git a/internal/tss/store/store_test.go b/internal/tss/store/store_test.go index 2987a27..ce9989b 100644 --- a/internal/tss/store/store_test.go +++ b/internal/tss/store/store_test.go @@ -19,7 +19,7 @@ func TestUpsertTransaction(t *testing.T) { dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) require.NoError(t, err) defer dbConnectionPool.Close() - store := NewStore(dbConnectionPool) + store, _ := NewStore(dbConnectionPool) t.Run("insert", func(t *testing.T) { _ = store.UpsertTransaction(context.Background(), "www.stellar.org", "hash", "xdr", tss.RPCTXStatus{OtherStatus: tss.NewStatus}) @@ -52,7 +52,7 @@ func TestUpsertTry(t *testing.T) { dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) require.NoError(t, err) defer dbConnectionPool.Close() - store := NewStore(dbConnectionPool) + store, _ := NewStore(dbConnectionPool) t.Run("insert", func(t *testing.T) { code := tss.RPCTXCode{OtherCodes: tss.NewCode} _ = store.UpsertTry(context.Background(), "hash", "feebumptxhash", "feebumptxxdr", code) From b4f605228f51d54cd52bea355535e3286b2f82b7 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Mon, 30 Sep 2024 15:46:36 -0700 Subject: [PATCH 088/113] removing test case that is not relevant anymore --- internal/tss/channels/rpc_caller_channel_test.go | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/internal/tss/channels/rpc_caller_channel_test.go b/internal/tss/channels/rpc_caller_channel_test.go index a46d063..04bcc4f 100644 --- a/internal/tss/channels/rpc_caller_channel_test.go +++ b/internal/tss/channels/rpc_caller_channel_test.go @@ -91,21 +91,6 @@ func TestReceivee(t *testing.T) { routerMock.AssertNotCalled(t, "Route", payload) }) - t.Run("payload_not_routed", func(t *testing.T) { - rpcResp := tss.RPCSendTxResponse{ - Status: tss.RPCTXStatus{RPCStatus: entities.PendingStatus}, - } - payload.RpcSubmitTxResponse = rpcResp - - txManagerMock. - On("BuildAndSubmitTransaction", context.Background(), RPCCallerChannelName, payload). - Return(rpcResp, nil). - Once() - - channel.Receive(payload) - - routerMock.AssertNotCalled(t, "Route", payload) - }) t.Run("payload_routed", func(t *testing.T) { rpcResp := tss.RPCSendTxResponse{ Status: tss.RPCTXStatus{RPCStatus: entities.ErrorStatus}, From 9be1beb8c05bb1363a86950548d3dc1763dea822 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Mon, 30 Sep 2024 16:03:49 -0700 Subject: [PATCH 089/113] changes based on prev pr comments --- internal/serve/serve.go | 8 -------- 1 file changed, 8 deletions(-) diff --git a/internal/serve/serve.go b/internal/serve/serve.go index 5536d9d..8000a71 100644 --- a/internal/serve/serve.go +++ b/internal/serve/serve.go @@ -222,14 +222,6 @@ func initHandlerDeps(cfg Configs) (handlerDeps, error) { MinWaitBtwnRetriesMS: cfg.ErrorHandlerServiceJitterChannelMinWaitBtwnRetriesMS, }) - errorNonJitterChannelConfigs := tsschannel.ErrorNonJitterChannelConfigs{ - TxManager: txManager, - MaxBufferSize: cfg.ErrorHandlerServiceJitterChannelBufferSize, - MaxWorkers: cfg.ErrorHandlerServiceJitterChannelMaxWorkers, - MaxRetries: cfg.ErrorHandlerServiceJitterChannelMaxRetries, - WaitBtwnRetriesMS: cfg.ErrorHandlerServiceJitterChannelMinWaitBtwnRetriesMS, - } - errorNonJitterChannel := tsschannel.NewErrorNonJitterChannel(tsschannel.ErrorNonJitterChannelConfigs{ TxManager: txManager, MaxBufferSize: cfg.ErrorHandlerServiceJitterChannelBufferSize, From 0df353747b7950d22cce62d75fd64b5fc899b749 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Mon, 30 Sep 2024 16:13:34 -0700 Subject: [PATCH 090/113] remove fmt.Println --- internal/tss/channels/error_non_jitter_channel.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/tss/channels/error_non_jitter_channel.go b/internal/tss/channels/error_non_jitter_channel.go index af8a4f9..c7322ec 100644 --- a/internal/tss/channels/error_non_jitter_channel.go +++ b/internal/tss/channels/error_non_jitter_channel.go @@ -2,7 +2,6 @@ package channels import ( "context" - "fmt" "slices" "time" @@ -55,7 +54,6 @@ func (p *errorNonJitterPool) Receive(payload tss.Payload) { ctx := context.Background() var i int for i = 0; i < p.MaxRetries; i++ { - fmt.Println(i) time.Sleep(time.Duration(p.WaitBtwnRetriesMS) * time.Microsecond) rpcSendResp, err := p.TxManager.BuildAndSubmitTransaction(ctx, ErrorNonJitterChannelName, payload) if err != nil { From 8c445f496b23192d1f04d631fe9ec2e019782622 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Mon, 30 Sep 2024 18:43:05 -0700 Subject: [PATCH 091/113] merge latest error_handler_service branch + small changes --- internal/serve/serve.go | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/internal/serve/serve.go b/internal/serve/serve.go index 09a457b..abc09ce 100644 --- a/internal/serve/serve.go +++ b/internal/serve/serve.go @@ -187,14 +187,13 @@ func initHandlerDeps(cfg Configs) (handlerDeps, error) { } go ensureChannelAccounts(channelAccountService, int64(cfg.NumberOfChannelAccounts)) - // TSS - txServiceOpts := tssservices.TransactionServiceOptions{ + // TSS setup + tssTxService, err := tssservices.NewTransactionService(tssservices.TransactionServiceOptions{ DistributionAccountSignatureClient: cfg.DistributionAccountSignatureClient, ChannelAccountSignatureClient: cfg.ChannelAccountSignatureClient, HorizonClient: &horizonClient, BaseFee: int64(cfg.BaseFee), - } - tssTxService, err := tssservices.NewTransactionService(txServiceOpts) + }) if err != nil { return handlerDeps{}, fmt.Errorf("instantiating tss transaction service: %w", err) } @@ -238,15 +237,13 @@ func initHandlerDeps(cfg Configs) (handlerDeps, error) { }) httpClient = http.Client{Timeout: time.Duration(30 * time.Second)} - webhookChannelConfigs := tsschannel.WebhookChannelConfigs{ + webhookChannel := tsschannel.NewWebhookChannel(tsschannel.WebhookChannelConfigs{ HTTPClient: &httpClient, MaxBufferSize: cfg.WebhookHandlerServiceChannelMaxBufferSize, MaxWorkers: cfg.WebhookHandlerServiceChannelMaxWorkers, MaxRetries: cfg.WebhookHandlerServiceChannelMaxRetries, MinWaitBtwnRetriesMS: cfg.WebhookHandlerServiceChannelMinWaitBtwnRetriesMS, - } - - webhookChannel := tsschannel.NewWebhookChannel(webhookChannelConfigs) + }) router := tssrouter.NewRouter(tssrouter.RouterConfigs{ RPCCallerChannel: rpcCallerChannel, From 2caf65b505103f9c8842f54ba67a46b03abd44df Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Mon, 30 Sep 2024 18:52:51 -0700 Subject: [PATCH 092/113] %s -> %w --- internal/tss/channels/webhook_channel.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/tss/channels/webhook_channel.go b/internal/tss/channels/webhook_channel.go index 31efae5..ecde40c 100644 --- a/internal/tss/channels/webhook_channel.go +++ b/internal/tss/channels/webhook_channel.go @@ -51,14 +51,14 @@ func (p *webhookPool) Receive(payload tss.Payload) { resp := tssutils.PayloadTOTSSResponse(payload) jsonData, err := json.Marshal(resp) if err != nil { - log.Errorf("WebhookHandlerServiceChannel: error marshaling payload: %s", err.Error()) + log.Errorf("WebhookHandlerServiceChannel: error marshaling payload: %w", err) return } var i int for i = 0; i < p.MaxRetries; i++ { resp, err := p.HTTPClient.Post(payload.WebhookURL, "application/json", bytes.NewBuffer(jsonData)) if err != nil { - log.Errorf("WebhookHandlerServiceChannel: error making POST request to webhook: %s", err.Error()) + log.Errorf("WebhookHandlerServiceChannel: error making POST request to webhook: %w", err) } defer resp.Body.Close() From ab2a546390ef5f3c8a64a7a648ddedd89b31ed80 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Mon, 30 Sep 2024 18:54:17 -0700 Subject: [PATCH 093/113] U -> u --- internal/tss/channels/error_jitter_channel.go | 6 +++--- internal/tss/channels/error_non_jitter_channel.go | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/tss/channels/error_jitter_channel.go b/internal/tss/channels/error_jitter_channel.go index 52097f2..06c52cf 100644 --- a/internal/tss/channels/error_jitter_channel.go +++ b/internal/tss/channels/error_jitter_channel.go @@ -63,14 +63,14 @@ func (p *errorJitterPool) Receive(payload tss.Payload) { time.Sleep(jitter(time.Duration(currentBackoff)) * time.Microsecond) rpcSendResp, err := p.TxManager.BuildAndSubmitTransaction(ctx, ErrorJitterChannelName, payload) if err != nil { - log.Errorf("%s: Unable to sign and submit transaction: %e", ErrorJitterChannelName, err) + log.Errorf("%s: unable to sign and submit transaction: %e", ErrorJitterChannelName, err) return } payload.RpcSubmitTxResponse = rpcSendResp if !slices.Contains(tss.JitterErrorCodes, rpcSendResp.Code.TxResultCode) { err = p.Router.Route(payload) if err != nil { - log.Errorf("%s: Unable to route payload: %e", ErrorJitterChannelName, err) + log.Errorf("%s: unable to route payload: %e", ErrorJitterChannelName, err) return } return @@ -81,7 +81,7 @@ func (p *errorJitterPool) Receive(payload tss.Payload) { // NOTE: Is this a good idea? Infinite tries per transaction ? err := p.Router.Route(payload) if err != nil { - log.Errorf("%s: Unable to route payload: %e", ErrorJitterChannelName, err) + log.Errorf("%s: unable to route payload: %e", ErrorJitterChannelName, err) } } } diff --git a/internal/tss/channels/error_non_jitter_channel.go b/internal/tss/channels/error_non_jitter_channel.go index c7322ec..52290d9 100644 --- a/internal/tss/channels/error_non_jitter_channel.go +++ b/internal/tss/channels/error_non_jitter_channel.go @@ -57,14 +57,14 @@ func (p *errorNonJitterPool) Receive(payload tss.Payload) { time.Sleep(time.Duration(p.WaitBtwnRetriesMS) * time.Microsecond) rpcSendResp, err := p.TxManager.BuildAndSubmitTransaction(ctx, ErrorNonJitterChannelName, payload) if err != nil { - log.Errorf("%s: Unable to sign and submit transaction: %e", ErrorNonJitterChannelName, err) + log.Errorf("%s: unable to sign and submit transaction: %e", ErrorNonJitterChannelName, err) return } payload.RpcSubmitTxResponse = rpcSendResp if !slices.Contains(tss.NonJitterErrorCodes, rpcSendResp.Code.TxResultCode) { err := p.Router.Route(payload) if err != nil { - log.Errorf("%s: Unable to route payload: %e", ErrorNonJitterChannelName, err) + log.Errorf("%s: unable to route payload: %e", ErrorNonJitterChannelName, err) return } return @@ -75,7 +75,7 @@ func (p *errorNonJitterPool) Receive(payload tss.Payload) { // NOTE: Is this a good idea? err := p.Router.Route(payload) if err != nil { - log.Errorf("%s: Unable to route payload: %e", ErrorNonJitterChannelName, err) + log.Errorf("%s: unable to route payload: %e", ErrorNonJitterChannelName, err) return } } From d576a251d256dab25c1ec305f282feb0e4f6f44b Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Mon, 30 Sep 2024 18:57:46 -0700 Subject: [PATCH 094/113] variable for channel name --- internal/tss/channels/webhook_channel.go | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/internal/tss/channels/webhook_channel.go b/internal/tss/channels/webhook_channel.go index ecde40c..cc13790 100644 --- a/internal/tss/channels/webhook_channel.go +++ b/internal/tss/channels/webhook_channel.go @@ -28,6 +28,8 @@ type webhookPool struct { MinWaitBtwnRetriesMS int } +var WebhookChannelName = "WebhookChannel" + var _ tss.Channel = (*webhookPool)(nil) func NewWebhookChannel(cfg WebhookChannelConfigs) *webhookPool { @@ -51,14 +53,14 @@ func (p *webhookPool) Receive(payload tss.Payload) { resp := tssutils.PayloadTOTSSResponse(payload) jsonData, err := json.Marshal(resp) if err != nil { - log.Errorf("WebhookHandlerServiceChannel: error marshaling payload: %w", err) + log.Errorf("%s: error marshaling payload: %e", WebhookChannelName, err) return } var i int for i = 0; i < p.MaxRetries; i++ { resp, err := p.HTTPClient.Post(payload.WebhookURL, "application/json", bytes.NewBuffer(jsonData)) if err != nil { - log.Errorf("WebhookHandlerServiceChannel: error making POST request to webhook: %w", err) + log.Errorf("%s: error making POST request to webhook: %e", WebhookChannelName, err) } defer resp.Body.Close() From 5de4041da0d2a2dc908247287d0cc3f9ccc764db Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Mon, 30 Sep 2024 19:12:30 -0700 Subject: [PATCH 095/113] account for NewStore returning an error --- internal/services/ingest_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/services/ingest_test.go b/internal/services/ingest_test.go index 720c15e..7405773 100644 --- a/internal/services/ingest_test.go +++ b/internal/services/ingest_test.go @@ -29,7 +29,7 @@ func TestGetLedgerTransactions(t *testing.T) { mockAppTracker := apptracker.MockAppTracker{} mockRPCService := RPCServiceMock{} mockRouter := tssrouter.MockRouter{} - tssStore := tssstore.NewStore(dbConnectionPool) + tssStore, _ := tssstore.NewStore(dbConnectionPool) ingestService, _ := NewIngestService(models, "ingestionLedger", &mockAppTracker, &mockRPCService, &mockRouter, tssStore) t.Run("all_ledger_transactions_in_single_gettransactions_call", func(t *testing.T) { rpcGetTransactionsResult := entities.RPCGetTransactionsResult{ @@ -121,7 +121,7 @@ func TestProcessTSSTransactions(t *testing.T) { mockAppTracker := apptracker.MockAppTracker{} mockRPCService := RPCServiceMock{} mockRouter := tssrouter.MockRouter{} - tssStore := tssstore.NewStore(dbConnectionPool) + tssStore, _ := tssstore.NewStore(dbConnectionPool) ingestService, _ := NewIngestService(models, "ingestionLedger", &mockAppTracker, &mockRPCService, &mockRouter, tssStore) t.Run("routes_to_tss_router", func(t *testing.T) { From a788011910f455c16b4841dda8c955669c40d8cb Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Mon, 30 Sep 2024 19:14:51 -0700 Subject: [PATCH 096/113] fix build --- internal/ingest/ingest.go | 5 ++++- internal/tss/store/store_test.go | 6 +++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/internal/ingest/ingest.go b/internal/ingest/ingest.go index dbf55bb..8316ca1 100644 --- a/internal/ingest/ingest.go +++ b/internal/ingest/ingest.go @@ -62,7 +62,10 @@ func setupDeps(cfg Configs) (services.IngestService, error) { if err != nil { return nil, fmt.Errorf("instantiating rpc service: %w", err) } - tssStore := tssstore.NewStore(dbConnectionPool) + tssStore, err := tssstore.NewStore(dbConnectionPool) + if err != nil { + return nil, fmt.Errorf("instantiating tss store: %w", err) + } tssRouterConfig := tssrouter.RouterConfigs{ WebhookChannel: cfg.WebhookChannel, } diff --git a/internal/tss/store/store_test.go b/internal/tss/store/store_test.go index b5042e3..f5b69c9 100644 --- a/internal/tss/store/store_test.go +++ b/internal/tss/store/store_test.go @@ -102,7 +102,7 @@ func TestGetTransaction(t *testing.T) { dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) require.NoError(t, err) defer dbConnectionPool.Close() - store := NewStore(dbConnectionPool) + store, _ := NewStore(dbConnectionPool) t.Run("transaction_exists", func(t *testing.T) { status := tss.RPCTXStatus{OtherStatus: tss.NewStatus} _ = store.UpsertTransaction(context.Background(), "localhost:8000", "hash", "xdr", status) @@ -124,7 +124,7 @@ func TestGetTry(t *testing.T) { dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) require.NoError(t, err) defer dbConnectionPool.Close() - store := NewStore(dbConnectionPool) + store, _ := NewStore(dbConnectionPool) t.Run("try_exists", func(t *testing.T) { code := tss.RPCTXCode{OtherCodes: tss.NewCode} _ = store.UpsertTry(context.Background(), "hash", "feebumptxhash", "feebumptxxdr", code) @@ -146,7 +146,7 @@ func TestGetTryByXDR(t *testing.T) { dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) require.NoError(t, err) defer dbConnectionPool.Close() - store := NewStore(dbConnectionPool) + store, _ := NewStore(dbConnectionPool) t.Run("try_exists", func(t *testing.T) { code := tss.RPCTXCode{OtherCodes: tss.NewCode} _ = store.UpsertTry(context.Background(), "hash", "feebumptxhash", "feebumptxxdr", code) From 22b04380a7498bb0a8440b38182c45c6ed76dad8 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Mon, 30 Sep 2024 19:59:00 -0700 Subject: [PATCH 097/113] commit #1 --- internal/serve/serve.go | 8 +++++ internal/tss/services/pool_populator.go | 40 +++++++++++++++++++++++++ internal/tss/store/store.go | 13 ++++++++ 3 files changed, 61 insertions(+) create mode 100644 internal/tss/services/pool_populator.go diff --git a/internal/serve/serve.go b/internal/serve/serve.go index abc09ce..10854db 100644 --- a/internal/serve/serve.go +++ b/internal/serve/serve.go @@ -100,6 +100,7 @@ type handlerDeps struct { ErrorNonJitterChannel tss.Channel WebhookChannel tss.Channel TSSRouter tssrouter.Router + PoolPopulator tssservices.PoolPopulator // Error Tracker AppTracker apptracker.AppTracker } @@ -116,6 +117,7 @@ func Serve(cfg Configs) error { Handler: handler(deps), OnStarting: func() { log.Infof("Starting Wallet Backend server on port %d", cfg.Port) + go deps.PoolPopulator.PopulatePools() }, OnStopping: func() { log.Info("Stopping Wallet Backend server") @@ -256,6 +258,11 @@ func initHandlerDeps(cfg Configs) (handlerDeps, error) { errorJitterChannel.SetRouter(router) errorNonJitterChannel.SetRouter(router) + poolPopulator, err := tssservices.NewPoolPopulator(router, store) + if err != nil { + return handlerDeps{}, fmt.Errorf("instantiating tss pool populator") + } + return handlerDeps{ Models: models, SignatureVerifier: signatureVerifier, @@ -270,6 +277,7 @@ func initHandlerDeps(cfg Configs) (handlerDeps, error) { ErrorNonJitterChannel: errorNonJitterChannel, WebhookChannel: webhookChannel, TSSRouter: router, + PoolPopulator: poolPopulator, }, nil } diff --git a/internal/tss/services/pool_populator.go b/internal/tss/services/pool_populator.go new file mode 100644 index 0000000..3849b75 --- /dev/null +++ b/internal/tss/services/pool_populator.go @@ -0,0 +1,40 @@ +package services + +import ( + "fmt" + + "github.com/stellar/wallet-backend/internal/tss/router" + "github.com/stellar/wallet-backend/internal/tss/store" +) + +type PoolPopulator interface { + PopulatePools() error +} + +type poolPopulator struct { + Router router.Router + Store store.Store +} + +func NewPoolPopulator(router router.Router, store store.Store) (*poolPopulator, error) { + if router == nil { + return nil, fmt.Errorf("router is nil") + } + if store == nil { + return nil, fmt.Errorf("store is nil") + } + return &poolPopulator{ + Router: router, + Store: store, + }, nil +} + +func (p *poolPopulator) PopulatePools() error { + /* + 1. Get all txns of status new + 2. get the latest try associated with them + 3. If the latest try is of status rpc fail or unmarshall error, check timebounds, and iff time bounds exceeded, try it again + */ + return nil + +} diff --git a/internal/tss/store/store.go b/internal/tss/store/store.go index 8fa0e51..ac3e1bc 100644 --- a/internal/tss/store/store.go +++ b/internal/tss/store/store.go @@ -131,3 +131,16 @@ func (s *store) GetTryByXDR(ctx context.Context, xdr string) (Try, error) { } return try, nil } + +func (s *store) GetTransactionsWithStatus(ctx context.Context, status string) ([]Transaction, error) { + q := `SELECT * from tss_transactions where status = $1` + var transactions []Transaction + err := s.DB.SelectContext(ctx, &transactions, q, status) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return []Transaction{}, nil + } + return []Transaction{}, fmt.Errorf("getting transactions: %w", err) + } + return transactions, nil +} From ef1c7bebed853f0aae88ffbec9248e09139f3232 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Wed, 2 Oct 2024 19:49:39 -0700 Subject: [PATCH 098/113] tss pool populator --- .../2024-08-28.0-tss_transactions.sql | 4 +- internal/serve/serve.go | 18 +- internal/services/ingest.go | 2 +- internal/services/ingest_test.go | 3 +- internal/tss/channels/webhook_channel.go | 31 ++- internal/tss/channels/webhook_channel_test.go | 18 ++ internal/tss/router/router.go | 4 + internal/tss/router/router_test.go | 31 ++- internal/tss/services/pool_populator.go | 235 +++++++++++++++++- internal/tss/services/pool_populator_test.go | 225 +++++++++++++++++ internal/tss/services/transaction_manager.go | 4 +- .../tss/services/transaction_manager_test.go | 60 ++--- internal/tss/services/transaction_service.go | 2 +- internal/tss/store/store.go | 37 ++- internal/tss/store/store_test.go | 147 ++++++++--- internal/tss/types.go | 10 +- internal/tss/types_test.go | 2 + internal/tss/utils/helpers.go | 1 + 18 files changed, 730 insertions(+), 104 deletions(-) create mode 100644 internal/tss/services/pool_populator_test.go diff --git a/internal/db/migrations/2024-08-28.0-tss_transactions.sql b/internal/db/migrations/2024-08-28.0-tss_transactions.sql index 9eba501..500c550 100644 --- a/internal/db/migrations/2024-08-28.0-tss_transactions.sql +++ b/internal/db/migrations/2024-08-28.0-tss_transactions.sql @@ -14,7 +14,9 @@ CREATE TABLE tss_transaction_submission_tries ( try_transaction_hash TEXT PRIMARY KEY, original_transaction_hash TEXT NOT NULL, try_transaction_xdr TEXT NOT NULL, - status INTEGER NOT NULL, + status TEXT NOT NULL, + code INTEGER NOT NULL, + result_xdr TEXT NOT NULL, updated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL ); diff --git a/internal/serve/serve.go b/internal/serve/serve.go index 10854db..115c780 100644 --- a/internal/serve/serve.go +++ b/internal/serve/serve.go @@ -117,7 +117,7 @@ func Serve(cfg Configs) error { Handler: handler(deps), OnStarting: func() { log.Infof("Starting Wallet Backend server on port %d", cfg.Port) - go deps.PoolPopulator.PopulatePools() + go populatePools(deps.PoolPopulator) }, OnStopping: func() { log.Info("Stopping Wallet Backend server") @@ -241,6 +241,7 @@ func initHandlerDeps(cfg Configs) (handlerDeps, error) { httpClient = http.Client{Timeout: time.Duration(30 * time.Second)} webhookChannel := tsschannel.NewWebhookChannel(tsschannel.WebhookChannelConfigs{ HTTPClient: &httpClient, + Store: store, MaxBufferSize: cfg.WebhookHandlerServiceChannelMaxBufferSize, MaxWorkers: cfg.WebhookHandlerServiceChannelMaxWorkers, MaxRetries: cfg.WebhookHandlerServiceChannelMaxRetries, @@ -258,7 +259,7 @@ func initHandlerDeps(cfg Configs) (handlerDeps, error) { errorJitterChannel.SetRouter(router) errorNonJitterChannel.SetRouter(router) - poolPopulator, err := tssservices.NewPoolPopulator(router, store) + poolPopulator, err := tssservices.NewPoolPopulator(router, store, rpcService) if err != nil { return handlerDeps{}, fmt.Errorf("instantiating tss pool populator") } @@ -281,6 +282,19 @@ func initHandlerDeps(cfg Configs) (handlerDeps, error) { }, nil } +func populatePools(poolPopulator tssservices.PoolPopulator) { + alertAfter := time.Minute * 10 + ticker := time.NewTicker(alertAfter) + ctx := context.Background() + + for range ticker.C { + err := poolPopulator.PopulatePools(context.Background()) + if err != nil { + log.Ctx(ctx).Error("Ensuring the number of channel accounts in the database...") + } + } +} + func ensureChannelAccounts(channelAccountService services.ChannelAccountService, numberOfChannelAccounts int64) { ctx := context.Background() log.Ctx(ctx).Info("Ensuring the number of channel accounts in the database...") diff --git a/internal/services/ingest.go b/internal/services/ingest.go index a432cf1..26ce0e5 100644 --- a/internal/services/ingest.go +++ b/internal/services/ingest.go @@ -206,7 +206,7 @@ func (m *ingestService) processTSSTransactions(ctx context.Context, ledgerTransa if err != nil { return fmt.Errorf("error unmarshaling resultxdr: %w", err) } - err = m.tssStore.UpsertTry(ctx, tssTry.OrigTxHash, tssTry.Hash, tssTry.XDR, code) + err = m.tssStore.UpsertTry(ctx, tssTry.OrigTxHash, tssTry.Hash, tssTry.XDR, status, code, tx.ResultXDR) if err != nil { return fmt.Errorf("error updating try: %w", err) } diff --git a/internal/services/ingest_test.go b/internal/services/ingest_test.go index 7405773..5bac262 100644 --- a/internal/services/ingest_test.go +++ b/internal/services/ingest_test.go @@ -142,7 +142,7 @@ func TestProcessTSSTransactions(t *testing.T) { } _ = tssStore.UpsertTransaction(context.Background(), "localhost:8000/webhook", "hash", "xdr", tss.RPCTXStatus{OtherStatus: tss.NewStatus}) - _ = tssStore.UpsertTry(context.Background(), "hash", "feebumphash", "feebumpxdr", tss.RPCTXCode{OtherCodes: tss.NewCode}) + _ = tssStore.UpsertTry(context.Background(), "hash", "feebumphash", "feebumpxdr", tss.RPCTXStatus{OtherStatus: tss.NewStatus}, tss.RPCTXCode{OtherCodes: tss.NewCode}, "") mockRouter. On("Route", mock.AnythingOfType("tss.Payload")). @@ -155,6 +155,7 @@ func TestProcessTSSTransactions(t *testing.T) { updatedTX, _ := tssStore.GetTransaction(context.Background(), "hash") assert.Equal(t, string(entities.SuccessStatus), updatedTX.Status) updatedTry, _ := tssStore.GetTry(context.Background(), "feebumphash") + assert.Equal(t, "AAAAAAAAAMj////9AAAAAA==", updatedTry.ResultXDR) assert.Equal(t, int32(xdr.TransactionResultCodeTxTooLate), updatedTry.Code) }) } diff --git a/internal/tss/channels/webhook_channel.go b/internal/tss/channels/webhook_channel.go index cc13790..5f7d9f6 100644 --- a/internal/tss/channels/webhook_channel.go +++ b/internal/tss/channels/webhook_channel.go @@ -2,19 +2,23 @@ package channels import ( "bytes" + "context" "encoding/json" + "fmt" "net/http" "time" "github.com/alitto/pond" "github.com/stellar/go/support/log" "github.com/stellar/wallet-backend/internal/tss" + "github.com/stellar/wallet-backend/internal/tss/store" tssutils "github.com/stellar/wallet-backend/internal/tss/utils" "github.com/stellar/wallet-backend/internal/utils" ) type WebhookChannelConfigs struct { HTTPClient utils.HTTPClient + Store store.Store MaxBufferSize int MaxWorkers int MaxRetries int @@ -23,6 +27,7 @@ type WebhookChannelConfigs struct { type webhookPool struct { Pool *pond.WorkerPool + Store store.Store HTTPClient utils.HTTPClient MaxRetries int MinWaitBtwnRetriesMS int @@ -36,6 +41,7 @@ func NewWebhookChannel(cfg WebhookChannelConfigs) *webhookPool { pool := pond.New(cfg.MaxBufferSize, cfg.MaxWorkers, pond.Strategy(pond.Balanced())) return &webhookPool{ Pool: pool, + Store: cfg.Store, HTTPClient: cfg.HTTPClient, MaxRetries: cfg.MaxRetries, MinWaitBtwnRetriesMS: cfg.MinWaitBtwnRetriesMS, @@ -57,19 +63,36 @@ func (p *webhookPool) Receive(payload tss.Payload) { return } var i int + sent := false + ctx := context.Background() for i = 0; i < p.MaxRetries; i++ { - resp, err := p.HTTPClient.Post(payload.WebhookURL, "application/json", bytes.NewBuffer(jsonData)) + httpResp, err := p.HTTPClient.Post(payload.WebhookURL, "application/json", bytes.NewBuffer(jsonData)) if err != nil { log.Errorf("%s: error making POST request to webhook: %e", WebhookChannelName, err) } - defer resp.Body.Close() + defer httpResp.Body.Close() - if resp.StatusCode == http.StatusOK { - return + if httpResp.StatusCode == http.StatusOK { + sent = true + err := p.Store.UpsertTransaction( + ctx, payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.RPCTXStatus{OtherStatus: tss.SentStatus}) + if err != nil { + fmt.Println(err) + log.Errorf("%s: error updating transaction status: %e", WebhookChannelName, err) + } + break } currentBackoff := p.MinWaitBtwnRetriesMS * (1 << i) time.Sleep(jitter(time.Duration(currentBackoff)) * time.Microsecond) } + if !sent { + err := p.Store.UpsertTransaction( + ctx, payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.RPCTXStatus{OtherStatus: tss.NotSentStatus}) + if err != nil { + log.Errorf("%s: error updating transaction status: %e", WebhookChannelName, err) + } + } + } func (p *webhookPool) Stop() { diff --git a/internal/tss/channels/webhook_channel_test.go b/internal/tss/channels/webhook_channel_test.go index 88e4cf3..29a9b69 100644 --- a/internal/tss/channels/webhook_channel_test.go +++ b/internal/tss/channels/webhook_channel_test.go @@ -2,21 +2,34 @@ package channels import ( "bytes" + "context" "encoding/json" "io" "net/http" "strings" "testing" + "github.com/stellar/wallet-backend/internal/db" + "github.com/stellar/wallet-backend/internal/db/dbtest" "github.com/stellar/wallet-backend/internal/tss" + "github.com/stellar/wallet-backend/internal/tss/store" tssutils "github.com/stellar/wallet-backend/internal/tss/utils" "github.com/stellar/wallet-backend/internal/utils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestWebhookHandlerServiceChannel(t *testing.T) { + dbt := dbtest.Open(t) + defer dbt.Close() + dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) + require.NoError(t, err) + defer dbConnectionPool.Close() + store, _ := store.NewStore(dbConnectionPool) mockHTTPClient := utils.MockHTTPClient{} cfg := WebhookChannelConfigs{ HTTPClient: &mockHTTPClient, + Store: store, MaxBufferSize: 1, MaxWorkers: 1, MaxRetries: 3, @@ -25,6 +38,8 @@ func TestWebhookHandlerServiceChannel(t *testing.T) { channel := NewWebhookChannel(cfg) payload := tss.Payload{} + payload.TransactionHash = "hash" + payload.TransactionXDR = "xdr" payload.WebhookURL = "www.stellar.org" jsonData, _ := json.Marshal(tssutils.PayloadTOTSSResponse(payload)) @@ -52,4 +67,7 @@ func TestWebhookHandlerServiceChannel(t *testing.T) { channel.Stop() mockHTTPClient.AssertNumberOfCalls(t, "Post", 2) + + tx, _ := store.GetTransaction(context.Background(), payload.TransactionHash) + assert.Equal(t, string(tss.SentStatus), tx.Status) } diff --git a/internal/tss/router/router.go b/internal/tss/router/router.go index b974203..a2b35bb 100644 --- a/internal/tss/router/router.go +++ b/internal/tss/router/router.go @@ -55,6 +55,10 @@ func (r *router) Route(payload tss.Payload) error { channel = r.WebhookChannel } } + case string(entities.SuccessStatus): + channel = r.WebhookChannel + case string(entities.FailedStatus): + channel = r.WebhookChannel default: // Do nothing for PENDING / DUPLICATE statuses return nil diff --git a/internal/tss/router/router_test.go b/internal/tss/router/router_test.go index e9d9660..3d0012e 100644 --- a/internal/tss/router/router_test.go +++ b/internal/tss/router/router_test.go @@ -50,6 +50,35 @@ func TestRouter(t *testing.T) { errorJitterChannel.AssertCalled(t, "Send", payload) }) + + t.Run("status_failure_routes_to_webhook_channel", func(t *testing.T) { + payload := tss.Payload{} + payload.RpcSubmitTxResponse.Status = tss.RPCTXStatus{RPCStatus: entities.FailedStatus} + + webhookChannel. + On("Send", payload). + Return(). + Once() + + _ = router.Route(payload) + + webhookChannel.AssertCalled(t, "Send", payload) + }) + + t.Run("status_success_routes_to_webhook_channel", func(t *testing.T) { + payload := tss.Payload{} + payload.RpcSubmitTxResponse.Status = tss.RPCTXStatus{RPCStatus: entities.SuccessStatus} + + webhookChannel. + On("Send", payload). + Return(). + Once() + + _ = router.Route(payload) + + webhookChannel.AssertCalled(t, "Send", payload) + }) + t.Run("status_error_routes_to_error_jitter_channel", func(t *testing.T) { for _, code := range tss.JitterErrorCodes { payload := tss.Payload{ @@ -119,7 +148,7 @@ func TestRouter(t *testing.T) { webhookChannel.AssertCalled(t, "Send", payload) } }) - t.Run("get_ingest_resp_always_routes_to_webhook_cbannel", func(t *testing.T) { + t.Run("get_ingest_resp_always_routes_to_webhook_channel", func(t *testing.T) { payload := tss.Payload{ RpcGetIngestTxResponse: tss.RPCGetIngestTxResponse{ Status: entities.SuccessStatus, diff --git a/internal/tss/services/pool_populator.go b/internal/tss/services/pool_populator.go index 3849b75..fb47fa5 100644 --- a/internal/tss/services/pool_populator.go +++ b/internal/tss/services/pool_populator.go @@ -1,40 +1,251 @@ package services import ( + "context" "fmt" + "slices" + "time" + "github.com/stellar/go/txnbuild" + "github.com/stellar/go/xdr" + "github.com/stellar/wallet-backend/internal/entities" + "github.com/stellar/wallet-backend/internal/services" + "github.com/stellar/wallet-backend/internal/tss" "github.com/stellar/wallet-backend/internal/tss/router" "github.com/stellar/wallet-backend/internal/tss/store" ) type PoolPopulator interface { - PopulatePools() error + PopulatePools(ctx context.Context) error } type poolPopulator struct { - Router router.Router - Store store.Store + Router router.Router + Store store.Store + RPCService services.RPCService } -func NewPoolPopulator(router router.Router, store store.Store) (*poolPopulator, error) { +func NewPoolPopulator(router router.Router, store store.Store, rpcService services.RPCService) (*poolPopulator, error) { if router == nil { return nil, fmt.Errorf("router is nil") } if store == nil { return nil, fmt.Errorf("store is nil") } + if rpcService == nil { + return nil, fmt.Errorf("rpcservice is nil") + } return &poolPopulator{ - Router: router, - Store: store, + Router: router, + Store: store, + RPCService: rpcService, }, nil } -func (p *poolPopulator) PopulatePools() error { - /* - 1. Get all txns of status new - 2. get the latest try associated with them - 3. If the latest try is of status rpc fail or unmarshall error, check timebounds, and iff time bounds exceeded, try it again - */ +func (p *poolPopulator) PopulatePools(ctx context.Context) error { + + err := p.routeNewTransactions() + if err != nil { + return fmt.Errorf("error routing new transactions: %w", err) + } + + err = p.routeErrorTransactions() + if err != nil { + return fmt.Errorf("error routing new transactions: %w", err) + } + + err = p.routeFinalTransactions(tss.RPCTXStatus{RPCStatus: entities.FailedStatus}) + if err != nil { + return fmt.Errorf("error routing failed transactions: %w", err) + } + + err = p.routeFinalTransactions(tss.RPCTXStatus{RPCStatus: entities.SuccessStatus}) + if err != nil { + return fmt.Errorf("error routing successful transactions: %w", err) + } + + err = p.routeNotSentTransactions() + if err != nil { + return fmt.Errorf("error routing not_sent transactions: %w", err) + } return nil +} +func (p *poolPopulator) routeNewTransactions() error { + ctx := context.Background() + newTxns, err := p.Store.GetTransactionsWithStatus(ctx, tss.RPCTXStatus{OtherStatus: tss.NewStatus}) + if err != nil { + return fmt.Errorf("unable to get transactions: %w", err) + } + for _, txn := range newTxns { + payload := tss.Payload{ + TransactionHash: txn.Hash, + TransactionXDR: txn.XDR, + WebhookURL: txn.WebhookURL, + } + try, err := p.Store.GetLatestTry(ctx, txn.Hash) + if err != nil { + return fmt.Errorf("getting latest try for transaction: %w", err) + } + if try == (store.Try{}) { + // there is no try for this transactionm - route to RPC caller channel + payload.RpcSubmitTxResponse.Status = tss.RPCTXStatus{OtherStatus: tss.NewStatus} + } else { + /* + if there is a try for this transaction, check to see if it is + submitted to RPC first. If status is NOT_FOUND, make sure + that the latest try for this transaction is past it's timebounds + before trying to re-submit the transaction. If the status is either + SUCCESS or FAILED, build a payload that will be routed to the Webhook + channel directly + */ + getTransactionResult, err := p.RPCService.GetTransaction(try.Hash) + if err != nil { + return fmt.Errorf("getting transaction: %w", err) + } + if getTransactionResult.Status == entities.NotFoundStatus { + genericTx, err := txnbuild.TransactionFromXDR(try.XDR) + if err != nil { + fmt.Println(txn.XDR) + return fmt.Errorf("unmarshaling tx from xdr string: %w", err) + } + feeBumpTx, unpackable := genericTx.FeeBump() + if !unpackable { + return fmt.Errorf("fee bump transaction cannot be unpacked: %w", err) + } + timeBounds := feeBumpTx.InnerTransaction().ToXDR().Preconditions().TimeBounds + if time.Now().Before(time.Unix(int64(timeBounds.MaxTime), 0)) { + continue + } + // route to the RPC Caller channel + payload.RpcSubmitTxResponse.Status = tss.RPCTXStatus{OtherStatus: tss.NewStatus} + } else { + getIngestTxResponse, err := tss.ParseToRPCGetIngestTxResponse(getTransactionResult, err) + if err != nil { + return fmt.Errorf("parsing rpc reponse: %w", err) + } + payload.RpcGetIngestTxResponse = getIngestTxResponse + } + } + err = p.Router.Route(payload) + if err != nil { + return fmt.Errorf("unable to route payload: %w", err) + } + } + return nil +} + +func (p *poolPopulator) routeErrorTransactions() error { + ctx := context.Background() + errorTxns, err := p.Store.GetTransactionsWithStatus(ctx, tss.RPCTXStatus{RPCStatus: entities.ErrorStatus}) + if err != nil { + return fmt.Errorf("unable to get transactions: %w", err) + } + for _, txn := range errorTxns { + payload := tss.Payload{ + TransactionHash: txn.Hash, + TransactionXDR: txn.XDR, + WebhookURL: txn.WebhookURL, + } + try, err := p.Store.GetLatestTry(ctx, txn.Hash) + if err != nil { + return fmt.Errorf("gretting latest try for transaction: %w", err) + } + if slices.Contains(tss.FinalErrorCodes, xdr.TransactionResultCode(try.Code)) { + // route to webhook channel + payload.RpcSubmitTxResponse = tss.RPCSendTxResponse{ + TransactionHash: try.Hash, + TransactionXDR: try.XDR, + Status: tss.RPCTXStatus{RPCStatus: entities.ErrorStatus}, + Code: tss.RPCTXCode{TxResultCode: xdr.TransactionResultCode(try.Code)}, + ErrorResultXDR: try.ResultXDR, + } + } else if try.Code == int32(tss.RPCFailCode) || try.Code == int32(tss.UnmarshalBinaryCode) { + // check for timebounds first and route iff out of timebounds route to errorchannel + genericTx, err := txnbuild.TransactionFromXDR(try.XDR) + if err != nil { + return fmt.Errorf("unmarshaling tx from xdr string: %w", err) + } + feeBumpTx, unpackable := genericTx.FeeBump() + if !unpackable { + return fmt.Errorf("fee bump transaction cannot be unpacked: %w", err) + } + timeBounds := feeBumpTx.InnerTransaction().ToXDR().Preconditions().TimeBounds + if time.Now().Before(time.Unix(int64(timeBounds.MaxTime), 0)) { + continue + } + payload.RpcSubmitTxResponse = tss.RPCSendTxResponse{ + TransactionHash: try.Hash, + TransactionXDR: try.XDR, + Status: tss.RPCTXStatus{RPCStatus: entities.TryAgainLaterStatus}, + } + + } + err = p.Router.Route(payload) + if err != nil { + return fmt.Errorf("unable to route payload: %w", err) + } + } + return nil +} + +func (p *poolPopulator) routeFinalTransactions(status tss.RPCTXStatus) error { + ctx := context.Background() + failedTxns, err := p.Store.GetTransactionsWithStatus(ctx, status) + if err != nil { + return fmt.Errorf("unable to get transactions: %w", err) + } + for _, txn := range failedTxns { + payload := tss.Payload{ + TransactionHash: txn.Hash, + TransactionXDR: txn.XDR, + WebhookURL: txn.WebhookURL, + } + try, err := p.Store.GetLatestTry(ctx, txn.Hash) + if err != nil { + return fmt.Errorf("gretting latest try for transaction: %w", err) + } + payload.RpcGetIngestTxResponse = tss.RPCGetIngestTxResponse{ + Status: status.RPCStatus, + Code: tss.RPCTXCode{TxResultCode: xdr.TransactionResultCode(try.Code)}, + EnvelopeXDR: try.XDR, + ResultXDR: try.ResultXDR, + } + err = p.Router.Route(payload) + if err != nil { + return fmt.Errorf("unable to route payload: %w", err) + } + } + return nil +} + +func (p *poolPopulator) routeNotSentTransactions() error { + ctx := context.Background() + notSentTxns, err := p.Store.GetTransactionsWithStatus(ctx, tss.RPCTXStatus{OtherStatus: tss.NotSentStatus}) + if err != nil { + return fmt.Errorf("unable to get transactions: %w", err) + } + for _, txn := range notSentTxns { + payload := tss.Payload{ + TransactionHash: txn.Hash, + TransactionXDR: txn.XDR, + WebhookURL: txn.WebhookURL, + } + try, err := p.Store.GetLatestTry(ctx, txn.Hash) + if err != nil { + return fmt.Errorf("gretting latest try for transaction: %w", err) + } + payload.RpcSubmitTxResponse = tss.RPCSendTxResponse{ + TransactionHash: try.Hash, + TransactionXDR: try.XDR, + Status: tss.RPCTXStatus{RPCStatus: entities.RPCStatus(try.Status)}, + Code: tss.RPCTXCode{TxResultCode: xdr.TransactionResultCode(try.Code)}, + ErrorResultXDR: try.ResultXDR, + } + err = p.Router.Route(payload) + if err != nil { + return fmt.Errorf("unable to route payload: %w", err) + } + } + return nil } diff --git a/internal/tss/services/pool_populator_test.go b/internal/tss/services/pool_populator_test.go new file mode 100644 index 0000000..b9c88c7 --- /dev/null +++ b/internal/tss/services/pool_populator_test.go @@ -0,0 +1,225 @@ +package services + +import ( + "context" + "testing" + + "github.com/stellar/go/xdr" + "github.com/stellar/wallet-backend/internal/db" + "github.com/stellar/wallet-backend/internal/db/dbtest" + "github.com/stellar/wallet-backend/internal/entities" + "github.com/stellar/wallet-backend/internal/services" + "github.com/stellar/wallet-backend/internal/tss" + "github.com/stellar/wallet-backend/internal/tss/router" + "github.com/stellar/wallet-backend/internal/tss/store" + "github.com/stellar/wallet-backend/internal/tss/utils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestRouteNewTransactions(t *testing.T) { + dbt := dbtest.Open(t) + defer dbt.Close() + + dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) + require.NoError(t, err) + defer dbConnectionPool.Close() + store, _ := store.NewStore(dbConnectionPool) + mockRouter := router.MockRouter{} + mockRPCSerive := services.RPCServiceMock{} + populator, _ := NewPoolPopulator(&mockRouter, store, &mockRPCSerive) + t.Run("tx_has_no_try", func(t *testing.T) { + _ = store.UpsertTransaction(context.Background(), "localhost:8000/webhook", "hash", "xdr", tss.RPCTXStatus{OtherStatus: tss.NewStatus}) + + expectedPayload := tss.Payload{ + TransactionHash: "hash", + TransactionXDR: "xdr", + WebhookURL: "localhost:8000/webhook", + RpcSubmitTxResponse: tss.RPCSendTxResponse{Status: tss.RPCTXStatus{OtherStatus: tss.NewStatus}}, + } + mockRouter. + On("Route", expectedPayload). + Return(nil). + Once() + + err := populator.routeNewTransactions() + assert.Empty(t, err) + }) + + t.Run("tx_has_try", func(t *testing.T) { + _ = store.UpsertTransaction(context.Background(), "localhost:8000/webhook", "hash", "xdr", tss.RPCTXStatus{OtherStatus: tss.NewStatus}) + _ = store.UpsertTry(context.Background(), "hash", "feebumphash", "feebumpxdr", tss.RPCTXStatus{OtherStatus: tss.NewStatus}, tss.RPCTXCode{OtherCodes: tss.NewCode}, "ABCD") + + rpcGetTransacrionResp := entities.RPCGetTransactionResult{ + Status: entities.ErrorStatus, + EnvelopeXDR: "envelopexdr", + ResultXDR: "AAAAAAARFy8AAAAAAAAAAQAAAAAAAAAYAAAAAMu8SHUN67hTUJOz3q+IrH9M/4dCVXaljeK6x1Ss20YWAAAAAA==", + CreatedAt: "1234", + } + + mockRPCSerive. + On("GetTransaction", "feebumphash"). + Return(rpcGetTransacrionResp, nil). + Once() + + getIngestTxResp, _ := tss.ParseToRPCGetIngestTxResponse(rpcGetTransacrionResp, nil) + expectedPayload := tss.Payload{ + TransactionHash: "hash", + TransactionXDR: "xdr", + WebhookURL: "localhost:8000/webhook", + RpcGetIngestTxResponse: getIngestTxResp, + } + mockRouter. + On("Route", expectedPayload). + Return(nil). + Once() + + err := populator.routeNewTransactions() + assert.Empty(t, err) + }) + + t.Run("tx_not_found_timebounds_not_exceeded", func(t *testing.T) { + feeBumpTx := utils.BuildTestFeeBumpTransaction() + txXDRStr, _ := feeBumpTx.Base64() + _ = store.UpsertTransaction(context.Background(), "localhost:8000/webhook", "hash", "xdr", tss.RPCTXStatus{OtherStatus: tss.NewStatus}) + _ = store.UpsertTry(context.Background(), "hash", "feebumphash", txXDRStr, tss.RPCTXStatus{OtherStatus: tss.NewStatus}, tss.RPCTXCode{OtherCodes: tss.NewCode}, "ABCD") + + rpcGetTransacrionResp := entities.RPCGetTransactionResult{ + Status: entities.NotFoundStatus, + EnvelopeXDR: "envelopexdr", + } + + mockRPCSerive. + On("GetTransaction", "feebumphash"). + Return(rpcGetTransacrionResp, nil). + Once() + + err := populator.routeNewTransactions() + assert.Empty(t, err) + }) +} + +func TestRouteErrorTransactions(t *testing.T) { + dbt := dbtest.Open(t) + defer dbt.Close() + + dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) + require.NoError(t, err) + defer dbConnectionPool.Close() + store, _ := store.NewStore(dbConnectionPool) + mockRouter := router.MockRouter{} + mockRPCSerive := services.RPCServiceMock{} + populator, _ := NewPoolPopulator(&mockRouter, store, &mockRPCSerive) + + t.Run("tx_has_final_error_code", func(t *testing.T) { + _ = store.UpsertTransaction(context.Background(), "localhost:8000/webhook", "hash", "xdr", tss.RPCTXStatus{RPCStatus: entities.ErrorStatus}) + _ = store.UpsertTry(context.Background(), "hash", "feebumphash", "feebumpxdr", tss.RPCTXStatus{RPCStatus: entities.ErrorStatus}, tss.RPCTXCode{TxResultCode: xdr.TransactionResultCodeTxInsufficientBalance}, "ABCD") + + expectedPayload := tss.Payload{ + TransactionHash: "hash", + TransactionXDR: "xdr", + WebhookURL: "localhost:8000/webhook", + RpcSubmitTxResponse: tss.RPCSendTxResponse{ + TransactionHash: "feebumphash", + TransactionXDR: "feebumpxdr", + Status: tss.RPCTXStatus{RPCStatus: entities.ErrorStatus}, + Code: tss.RPCTXCode{TxResultCode: xdr.TransactionResultCodeTxInsufficientBalance}, + ErrorResultXDR: "ABCD", + }, + } + + mockRouter. + On("Route", expectedPayload). + Return(nil). + Once() + + err := populator.routeErrorTransactions() + assert.Empty(t, err) + }) + t.Run("tx_timebounds_not_exceeded", func(t *testing.T) { + feeBumpTx := utils.BuildTestFeeBumpTransaction() + txXDRStr, _ := feeBumpTx.Base64() + _ = store.UpsertTransaction(context.Background(), "localhost:8000/webhook", "hash", "xdr", tss.RPCTXStatus{RPCStatus: entities.ErrorStatus}) + _ = store.UpsertTry(context.Background(), "hash", "feebumphash", txXDRStr, tss.RPCTXStatus{RPCStatus: entities.ErrorStatus}, tss.RPCTXCode{OtherCodes: tss.RPCFailCode}, "ABCD") + + err := populator.routeErrorTransactions() + assert.Empty(t, err) + }) +} + +func TestRouteFinalTransactions(t *testing.T) { + dbt := dbtest.Open(t) + defer dbt.Close() + + dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) + require.NoError(t, err) + defer dbConnectionPool.Close() + store, _ := store.NewStore(dbConnectionPool) + mockRouter := router.MockRouter{} + mockRPCSerive := services.RPCServiceMock{} + populator, _ := NewPoolPopulator(&mockRouter, store, &mockRPCSerive) + + t.Run("route_successful_tx", func(t *testing.T) { + _ = store.UpsertTransaction(context.Background(), "localhost:8000/webhook", "hash", "xdr", tss.RPCTXStatus{RPCStatus: entities.SuccessStatus}) + _ = store.UpsertTry(context.Background(), "hash", "feebumphash", "feebumpxdr", tss.RPCTXStatus{RPCStatus: entities.SuccessStatus}, tss.RPCTXCode{TxResultCode: xdr.TransactionResultCodeTxSuccess}, "ABCD") + + expectedPayload := tss.Payload{ + TransactionHash: "hash", + TransactionXDR: "xdr", + WebhookURL: "localhost:8000/webhook", + RpcGetIngestTxResponse: tss.RPCGetIngestTxResponse{ + Status: entities.SuccessStatus, + Code: tss.RPCTXCode{TxResultCode: xdr.TransactionResultCodeTxSuccess}, + EnvelopeXDR: "feebumpxdr", + ResultXDR: "ABCD", + }, + } + + mockRouter. + On("Route", expectedPayload). + Return(nil). + Once() + + err = populator.routeFinalTransactions(tss.RPCTXStatus{RPCStatus: entities.SuccessStatus}) + assert.Empty(t, err) + }) +} + +func TestNotSentTransactions(t *testing.T) { + dbt := dbtest.Open(t) + defer dbt.Close() + + dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) + require.NoError(t, err) + defer dbConnectionPool.Close() + store, _ := store.NewStore(dbConnectionPool) + mockRouter := router.MockRouter{} + mockRPCSerive := services.RPCServiceMock{} + populator, _ := NewPoolPopulator(&mockRouter, store, &mockRPCSerive) + + t.Run("routes_not_sent_txns", func(t *testing.T) { + _ = store.UpsertTransaction(context.Background(), "localhost:8000/webhook", "hash", "xdr", tss.RPCTXStatus{OtherStatus: tss.NotSentStatus}) + _ = store.UpsertTry(context.Background(), "hash", "feebumphash", "feebumpxdr", tss.RPCTXStatus{RPCStatus: entities.SuccessStatus}, tss.RPCTXCode{TxResultCode: xdr.TransactionResultCodeTxSuccess}, "ABCD") + + expectedPayload := tss.Payload{ + TransactionHash: "hash", + TransactionXDR: "xdr", + WebhookURL: "localhost:8000/webhook", + RpcSubmitTxResponse: tss.RPCSendTxResponse{ + TransactionHash: "feebumphash", + TransactionXDR: "feebumpxdr", + Status: tss.RPCTXStatus{RPCStatus: entities.SuccessStatus}, + Code: tss.RPCTXCode{TxResultCode: xdr.TransactionResultCodeTxSuccess}, + ErrorResultXDR: "ABCD", + }, + } + + mockRouter. + On("Route", expectedPayload). + Return(nil). + Once() + + err = populator.routeNotSentTransactions() + assert.Empty(t, err) + }) +} diff --git a/internal/tss/services/transaction_manager.go b/internal/tss/services/transaction_manager.go index 5fa3c63..0a88573 100644 --- a/internal/tss/services/transaction_manager.go +++ b/internal/tss/services/transaction_manager.go @@ -48,14 +48,14 @@ func (t *transactionManager) BuildAndSubmitTransaction(ctx context.Context, chan return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to base64 fee bump transaction: %w", channelName, err) } - err = t.Store.UpsertTry(ctx, payload.TransactionHash, feeBumpTxHash, feeBumpTxXDR, tss.RPCTXCode{OtherCodes: tss.NewCode}) + err = t.Store.UpsertTry(ctx, payload.TransactionHash, feeBumpTxHash, feeBumpTxXDR, tss.RPCTXStatus{OtherStatus: tss.NewStatus}, tss.RPCTXCode{OtherCodes: tss.NewCode}, "") if err != nil { return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to upsert try in tries table: %w", channelName, err) } rpcResp, rpcErr := t.RPCService.SendTransaction(feeBumpTxXDR) rpcSendResp, parseErr := tss.ParseToRPCSendTxResponse(feeBumpTxHash, rpcResp, rpcErr) - err = t.Store.UpsertTry(ctx, payload.TransactionHash, feeBumpTxHash, feeBumpTxXDR, rpcSendResp.Code) + err = t.Store.UpsertTry(ctx, payload.TransactionHash, feeBumpTxHash, feeBumpTxXDR, rpcSendResp.Status, rpcSendResp.Code, rpcResp.ErrorResultXDR) if err != nil { return tss.RPCSendTxResponse{}, fmt.Errorf("%s: Unable to upsert try in tries table: %s", channelName, err.Error()) } diff --git a/internal/tss/services/transaction_manager_test.go b/internal/tss/services/transaction_manager_test.go index 9a5742a..7beb2fa 100644 --- a/internal/tss/services/transaction_manager_test.go +++ b/internal/tss/services/transaction_manager_test.go @@ -52,10 +52,8 @@ func TestBuildAndSubmitTransaction(t *testing.T) { assert.Equal(t, "channel: Unable to sign/build transaction: signing failed", err.Error()) - var status string - err = dbConnectionPool.GetContext(context.Background(), &status, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, string(tss.NewStatus), status) + tx, _ := store.GetTransaction(context.Background(), payload.TransactionHash) + assert.Equal(t, string(tss.NewStatus), tx.Status) }) t.Run("rpc_call_fail", func(t *testing.T) { @@ -78,15 +76,12 @@ func TestBuildAndSubmitTransaction(t *testing.T) { assert.Equal(t, "channel: RPC fail: RPC fail: RPC down", err.Error()) - var txStatus string - err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, txStatus, string(tss.NewStatus)) + tx, _ := store.GetTransaction(context.Background(), payload.TransactionHash) + assert.Equal(t, string(tss.NewStatus), tx.Status) - var tryStatus int - err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) - require.NoError(t, err) - assert.Equal(t, int(tss.RPCFailCode), tryStatus) + try, _ := store.GetTry(context.Background(), feeBumpTxHash) + assert.Equal(t, string(entities.ErrorStatus), try.Status) + assert.Equal(t, int32(tss.RPCFailCode), try.Code) }) t.Run("rpc_resp_empty_errorresult_xdr", func(t *testing.T) { @@ -114,16 +109,14 @@ func TestBuildAndSubmitTransaction(t *testing.T) { assert.Equal(t, tss.EmptyCode, resp.Code.OtherCodes) assert.Empty(t, err) - var txStatus string - err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, txStatus, string(entities.PendingStatus)) + tx, _ := store.GetTransaction(context.Background(), payload.TransactionHash) + assert.Equal(t, string(entities.PendingStatus), tx.Status) - var tryStatus int - err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) - require.NoError(t, err) - assert.Equal(t, int(tss.EmptyCode), tryStatus) + try, _ := store.GetTry(context.Background(), feeBumpTxHash) + assert.Equal(t, string(entities.PendingStatus), try.Status) + assert.Equal(t, int32(tss.EmptyCode), try.Code) }) + t.Run("rpc_resp_has_unparsable_errorresult_xdr", func(t *testing.T) { _ = store.UpsertTransaction(context.Background(), payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.RPCTXStatus{OtherStatus: tss.NewStatus}) sendResp := entities.RPCSendTransactionResult{ @@ -147,16 +140,14 @@ func TestBuildAndSubmitTransaction(t *testing.T) { assert.Equal(t, "channel: RPC fail: parse error result xdr string: unable to parse: unable to unmarshal errorResultXDR: ABCD", err.Error()) - var txStatus string - err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, txStatus, string(tss.NewStatus)) + tx, _ := store.GetTransaction(context.Background(), payload.TransactionHash) + assert.Equal(t, string(tss.NewStatus), tx.Status) - var tryStatus int - err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) - require.NoError(t, err) - assert.Equal(t, int(tss.UnmarshalBinaryCode), tryStatus) + try, _ := store.GetTry(context.Background(), feeBumpTxHash) + assert.Equal(t, string(entities.ErrorStatus), try.Status) + assert.Equal(t, int32(tss.UnmarshalBinaryCode), try.Code) }) + t.Run("rpc_returns_response", func(t *testing.T) { _ = store.UpsertTransaction(context.Background(), payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.RPCTXStatus{OtherStatus: tss.NewStatus}) sendResp := entities.RPCSendTransactionResult{ @@ -182,14 +173,11 @@ func TestBuildAndSubmitTransaction(t *testing.T) { assert.Equal(t, xdr.TransactionResultCodeTxTooLate, resp.Code.TxResultCode) assert.Empty(t, err) - var txStatus string - err = dbConnectionPool.GetContext(context.Background(), &txStatus, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, payload.TransactionHash) - require.NoError(t, err) - assert.Equal(t, string(entities.ErrorStatus), txStatus) + tx, _ := store.GetTransaction(context.Background(), payload.TransactionHash) + assert.Equal(t, string(entities.ErrorStatus), tx.Status) - var tryStatus int - err = dbConnectionPool.GetContext(context.Background(), &tryStatus, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, feeBumpTxHash) - require.NoError(t, err) - assert.Equal(t, int(xdr.TransactionResultCodeTxTooLate), tryStatus) + try, _ := store.GetTry(context.Background(), feeBumpTxHash) + assert.Equal(t, string(entities.ErrorStatus), try.Status) + assert.Equal(t, int32(xdr.TransactionResultCodeTxTooLate), try.Code) }) } diff --git a/internal/tss/services/transaction_service.go b/internal/tss/services/transaction_service.go index b65bae1..9ecf7d9 100644 --- a/internal/tss/services/transaction_service.go +++ b/internal/tss/services/transaction_service.go @@ -90,7 +90,7 @@ func (t *transactionService) SignAndBuildNewFeeBumpTransaction(ctx context.Conte Operations: originalTx.Operations(), BaseFee: int64(t.BaseFee), Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewTimeout(300), + TimeBounds: txnbuild.NewTimeout(120), }, IncrementSequenceNum: true, }, diff --git a/internal/tss/store/store.go b/internal/tss/store/store.go index ac3e1bc..42906c3 100644 --- a/internal/tss/store/store.go +++ b/internal/tss/store/store.go @@ -14,9 +14,11 @@ import ( type Store interface { GetTransaction(ctx context.Context, hash string) (Transaction, error) UpsertTransaction(ctx context.Context, WebhookURL string, txHash string, txXDR string, status tss.RPCTXStatus) error - UpsertTry(ctx context.Context, transactionHash string, feeBumpTxHash string, feeBumpTxXDR string, status tss.RPCTXCode) error + UpsertTry(ctx context.Context, transactionHash string, feeBumpTxHash string, feeBumpTxXDR string, status tss.RPCTXStatus, code tss.RPCTXCode, resultXDR string) error GetTry(ctx context.Context, hash string) (Try, error) GetTryByXDR(ctx context.Context, xdr string) (Try, error) + GetTransactionsWithStatus(ctx context.Context, status tss.RPCTXStatus) ([]Transaction, error) + GetLatestTry(ctx context.Context, txHash string) (Try, error) } var _ Store = (*store)(nil) @@ -39,7 +41,9 @@ type Try struct { Hash string `db:"try_transaction_hash"` OrigTxHash string `db:"original_transaction_hash"` XDR string `db:"try_transaction_xdr"` - Code int32 `db:"status"` + Status string `db:"status"` + Code int32 `db:"code"` + ResultXDR string `db:"result_xdr"` CreatedAt time.Time `db:"updated_at"` } @@ -72,20 +76,22 @@ func (s *store) UpsertTransaction(ctx context.Context, webhookURL string, txHash return nil } -func (s *store) UpsertTry(ctx context.Context, txHash string, feeBumpTxHash string, feeBumpTxXDR string, status tss.RPCTXCode) error { +func (s *store) UpsertTry(ctx context.Context, txHash string, feeBumpTxHash string, feeBumpTxXDR string, status tss.RPCTXStatus, code tss.RPCTXCode, resultXDR string) error { const q = ` INSERT INTO - tss_transaction_submission_tries (original_transaction_hash, try_transaction_hash, try_transaction_xdr, status) + tss_transaction_submission_tries (original_transaction_hash, try_transaction_hash, try_transaction_xdr, status, code, result_xdr) VALUES - ($1, $2, $3, $4) + ($1, $2, $3, $4, $5, $6) ON CONFLICT (try_transaction_hash) DO UPDATE SET original_transaction_hash = EXCLUDED.original_transaction_hash, try_transaction_xdr = EXCLUDED.try_transaction_xdr, status = EXCLUDED.status, + code = EXCLUDED.code, + result_xdr = EXCLUDED.result_xdr, updated_at = NOW(); ` - _, err := s.DB.ExecContext(ctx, q, txHash, feeBumpTxHash, feeBumpTxXDR, status.Code()) + _, err := s.DB.ExecContext(ctx, q, txHash, feeBumpTxHash, feeBumpTxXDR, status.Status(), code.Code(), resultXDR) if err != nil { return fmt.Errorf("inserting/updating tss try: %w", err) } @@ -132,10 +138,10 @@ func (s *store) GetTryByXDR(ctx context.Context, xdr string) (Try, error) { return try, nil } -func (s *store) GetTransactionsWithStatus(ctx context.Context, status string) ([]Transaction, error) { - q := `SELECT * from tss_transactions where status = $1` +func (s *store) GetTransactionsWithStatus(ctx context.Context, status tss.RPCTXStatus) ([]Transaction, error) { + q := `SELECT * from tss_transactions where current_status = $1` var transactions []Transaction - err := s.DB.SelectContext(ctx, &transactions, q, status) + err := s.DB.SelectContext(ctx, &transactions, q, status.Status()) if err != nil { if errors.Is(err, sql.ErrNoRows) { return []Transaction{}, nil @@ -144,3 +150,16 @@ func (s *store) GetTransactionsWithStatus(ctx context.Context, status string) ([ } return transactions, nil } + +func (s *store) GetLatestTry(ctx context.Context, txHash string) (Try, error) { + q := `SELECT * from tss_transaction_submission_tries where original_transaction_hash = $1 ORDER BY updated_at DESC LIMIT 1` + var try Try + err := s.DB.GetContext(ctx, &try, q, txHash) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return Try{}, nil + } + return Try{}, fmt.Errorf("getting latest trt: %w", err) + } + return try, nil +} diff --git a/internal/tss/store/store_test.go b/internal/tss/store/store_test.go index f5b69c9..2a2a3e1 100644 --- a/internal/tss/store/store_test.go +++ b/internal/tss/store/store_test.go @@ -23,20 +23,18 @@ func TestUpsertTransaction(t *testing.T) { t.Run("insert", func(t *testing.T) { _ = store.UpsertTransaction(context.Background(), "www.stellar.org", "hash", "xdr", tss.RPCTXStatus{OtherStatus: tss.NewStatus}) - var status string - err = dbConnectionPool.GetContext(context.Background(), &status, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, "hash") - require.NoError(t, err) - assert.Equal(t, status, string(tss.NewStatus)) + tx, _ := store.GetTransaction(context.Background(), "hash") + assert.Equal(t, "xdr", tx.XDR) + assert.Equal(t, string(tss.NewStatus), tx.Status) }) t.Run("update", func(t *testing.T) { _ = store.UpsertTransaction(context.Background(), "www.stellar.org", "hash", "xdr", tss.RPCTXStatus{OtherStatus: tss.NewStatus}) _ = store.UpsertTransaction(context.Background(), "www.stellar.org", "hash", "xdr", tss.RPCTXStatus{RPCStatus: entities.SuccessStatus}) - var status string - err = dbConnectionPool.GetContext(context.Background(), &status, `SELECT current_status FROM tss_transactions WHERE transaction_hash = $1`, "hash") - require.NoError(t, err) - assert.Equal(t, status, string(entities.SuccessStatus)) + tx, _ := store.GetTransaction(context.Background(), "hash") + assert.Equal(t, "xdr", tx.XDR) + assert.Equal(t, string(entities.SuccessStatus), tx.Status) var numRows int err = dbConnectionPool.GetContext(context.Background(), &numRows, `SELECT count(*) FROM tss_transactions WHERE transaction_hash = $1`, "hash") @@ -54,24 +52,32 @@ func TestUpsertTry(t *testing.T) { defer dbConnectionPool.Close() store, _ := NewStore(dbConnectionPool) t.Run("insert", func(t *testing.T) { + status := tss.RPCTXStatus{OtherStatus: tss.NewStatus} code := tss.RPCTXCode{OtherCodes: tss.NewCode} - _ = store.UpsertTry(context.Background(), "hash", "feebumptxhash", "feebumptxxdr", code) - - var status int - err = dbConnectionPool.GetContext(context.Background(), &status, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, "feebumptxhash") - require.NoError(t, err) - assert.Equal(t, status, int(tss.NewCode)) + resultXDR := "ABCD//" + err = store.UpsertTry(context.Background(), "hash", "feebumptxhash", "feebumptxxdr", status, code, resultXDR) + + try, _ := store.GetTry(context.Background(), "feebumptxhash") + assert.Equal(t, "hash", try.OrigTxHash) + assert.Equal(t, status.Status(), try.Status) + assert.Equal(t, code.Code(), int(try.Code)) + assert.Equal(t, resultXDR, try.ResultXDR) + assert.Equal(t, status.Status(), try.Status) }) t.Run("update_other_code", func(t *testing.T) { + status := tss.RPCTXStatus{OtherStatus: tss.NewStatus} code := tss.RPCTXCode{OtherCodes: tss.NewCode} - _ = store.UpsertTry(context.Background(), "hash", "feebumptxhash", "feebumptxxdr", code) + resultXDR := "ABCD//" + _ = store.UpsertTry(context.Background(), "hash", "feebumptxhash", "feebumptxxdr", status, code, resultXDR) code = tss.RPCTXCode{OtherCodes: tss.RPCFailCode} - _ = store.UpsertTry(context.Background(), "hash", "feebumptxhash", "feebumptxxdr", code) - var status int - err = dbConnectionPool.GetContext(context.Background(), &status, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, "feebumptxhash") - require.NoError(t, err) - assert.Equal(t, status, int(tss.RPCFailCode)) + _ = store.UpsertTry(context.Background(), "hash", "feebumptxhash", "feebumptxxdr", status, code, resultXDR) + + try, _ := store.GetTry(context.Background(), "feebumptxhash") + assert.Equal(t, "hash", try.OrigTxHash) + assert.Equal(t, status.Status(), try.Status) + assert.Equal(t, code.Code(), int(try.Code)) + assert.Equal(t, resultXDR, try.ResultXDR) var numRows int err = dbConnectionPool.GetContext(context.Background(), &numRows, `SELECT count(*) FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, "feebumptxhash") @@ -80,14 +86,18 @@ func TestUpsertTry(t *testing.T) { }) t.Run("update_tx_code", func(t *testing.T) { - code := tss.RPCTXCode{OtherCodes: tss.NewCode} - _ = store.UpsertTry(context.Background(), "hash", "feebumptxhash", "feebumptxxdr", code) + status := tss.RPCTXStatus{RPCStatus: entities.ErrorStatus} + code := tss.RPCTXCode{TxResultCode: xdr.TransactionResultCodeTxInsufficientFee} + resultXDR := "ABCD//" + _ = store.UpsertTry(context.Background(), "hash", "feebumptxhash", "feebumptxxdr", status, code, resultXDR) code = tss.RPCTXCode{TxResultCode: xdr.TransactionResultCodeTxSuccess} - _ = store.UpsertTry(context.Background(), "hash", "feebumptxhash", "feebumptxxdr", code) - var status int - err = dbConnectionPool.GetContext(context.Background(), &status, `SELECT status FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, "feebumptxhash") - require.NoError(t, err) - assert.Equal(t, status, int(xdr.TransactionResultCodeTxSuccess)) + _ = store.UpsertTry(context.Background(), "hash", "feebumptxhash", "feebumptxxdr", status, code, resultXDR) + + try, _ := store.GetTry(context.Background(), "feebumptxhash") + assert.Equal(t, "hash", try.OrigTxHash) + assert.Equal(t, status.Status(), try.Status) + assert.Equal(t, code.Code(), int(try.Code)) + assert.Equal(t, resultXDR, try.ResultXDR) var numRows int err = dbConnectionPool.GetContext(context.Background(), &numRows, `SELECT count(*) FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1`, "feebumptxhash") @@ -106,7 +116,9 @@ func TestGetTransaction(t *testing.T) { t.Run("transaction_exists", func(t *testing.T) { status := tss.RPCTXStatus{OtherStatus: tss.NewStatus} _ = store.UpsertTransaction(context.Background(), "localhost:8000", "hash", "xdr", status) + tx, err := store.GetTransaction(context.Background(), "hash") + assert.Equal(t, "xdr", tx.XDR) assert.Empty(t, err) @@ -126,10 +138,17 @@ func TestGetTry(t *testing.T) { defer dbConnectionPool.Close() store, _ := NewStore(dbConnectionPool) t.Run("try_exists", func(t *testing.T) { + status := tss.RPCTXStatus{OtherStatus: tss.NewStatus} code := tss.RPCTXCode{OtherCodes: tss.NewCode} - _ = store.UpsertTry(context.Background(), "hash", "feebumptxhash", "feebumptxxdr", code) + resultXDR := "ABCD//" + _ = store.UpsertTry(context.Background(), "hash", "feebumptxhash", "feebumptxxdr", status, code, resultXDR) + try, err := store.GetTry(context.Background(), "feebumptxhash") - assert.Equal(t, try.OrigTxHash, "hash") + + assert.Equal(t, "hash", try.OrigTxHash) + assert.Equal(t, status.Status(), try.Status) + assert.Equal(t, code.Code(), int(try.Code)) + assert.Equal(t, resultXDR, try.ResultXDR) assert.Empty(t, err) }) @@ -148,10 +167,17 @@ func TestGetTryByXDR(t *testing.T) { defer dbConnectionPool.Close() store, _ := NewStore(dbConnectionPool) t.Run("try_exists", func(t *testing.T) { + status := tss.RPCTXStatus{OtherStatus: tss.NewStatus} code := tss.RPCTXCode{OtherCodes: tss.NewCode} - _ = store.UpsertTry(context.Background(), "hash", "feebumptxhash", "feebumptxxdr", code) + resultXDR := "ABCD//" + _ = store.UpsertTry(context.Background(), "hash", "feebumptxhash", "feebumptxxdr", status, code, resultXDR) + try, err := store.GetTryByXDR(context.Background(), "feebumptxxdr") - assert.Equal(t, try.OrigTxHash, "hash") + + assert.Equal(t, "hash", try.OrigTxHash) + assert.Equal(t, status.Status(), try.Status) + assert.Equal(t, code.Code(), int(try.Code)) + assert.Equal(t, resultXDR, try.ResultXDR) assert.Empty(t, err) }) @@ -161,3 +187,62 @@ func TestGetTryByXDR(t *testing.T) { assert.Empty(t, err) }) } + +func TestGetTransactionsWithStatus(t *testing.T) { + dbt := dbtest.Open(t) + defer dbt.Close() + dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) + require.NoError(t, err) + defer dbConnectionPool.Close() + store, _ := NewStore(dbConnectionPool) + + t.Run("transactions_do_not_exist", func(t *testing.T) { + status := tss.RPCTXStatus{OtherStatus: tss.NewStatus} + txns, err := store.GetTransactionsWithStatus(context.Background(), status) + assert.Equal(t, 0, len(txns)) + assert.Empty(t, err) + }) + + t.Run("transactions_exist", func(t *testing.T) { + status := tss.RPCTXStatus{OtherStatus: tss.NewStatus} + _ = store.UpsertTransaction(context.Background(), "localhost:8000", "hash1", "xdr1", status) + _ = store.UpsertTransaction(context.Background(), "localhost:8000", "hash2", "xdr2", status) + + txns, err := store.GetTransactionsWithStatus(context.Background(), status) + + assert.Equal(t, 2, len(txns)) + assert.Equal(t, "hash1", txns[0].Hash) + assert.Equal(t, "hash2", txns[1].Hash) + assert.Empty(t, err) + }) +} + +func TestGetLatestTry(t *testing.T) { + dbt := dbtest.Open(t) + defer dbt.Close() + dbConnectionPool, err := db.OpenDBConnectionPool(dbt.DSN) + require.NoError(t, err) + defer dbConnectionPool.Close() + store, _ := NewStore(dbConnectionPool) + + t.Run("tries_do_not_exist", func(t *testing.T) { + try, err := store.GetLatestTry(context.Background(), "hash") + + assert.Equal(t, Try{}, try) + assert.Empty(t, err) + }) + + t.Run("tries_exist", func(t *testing.T) { + status := tss.RPCTXStatus{OtherStatus: tss.NewStatus} + code := tss.RPCTXCode{OtherCodes: tss.NewCode} + resultXDR := "ABCD//" + _ = store.UpsertTry(context.Background(), "hash", "feebumptxhash1", "feebumptxxdr1", status, code, resultXDR) + _ = store.UpsertTry(context.Background(), "hash", "feebumptxhash2", "feebumptxxdr2", status, code, resultXDR) + + try, err := store.GetLatestTry(context.Background(), "hash") + + assert.Equal(t, "feebumptxhash2", try.Hash) + assert.Empty(t, err) + }) + +} diff --git a/internal/tss/types.go b/internal/tss/types.go index b10876d..cdeb589 100644 --- a/internal/tss/types.go +++ b/internal/tss/types.go @@ -55,8 +55,10 @@ type OtherCodes int32 type TransactionResultCode int32 const ( - NewStatus OtherStatus = "NEW" - NoStatus OtherStatus = "" + NewStatus OtherStatus = "NEW" + NoStatus OtherStatus = "" + SentStatus OtherStatus = "SENT" + NotSentStatus OtherStatus = "NOT_SENT" ) type RPCTXStatus struct { @@ -121,7 +123,8 @@ type RPCSendTxResponse struct { Status RPCTXStatus // The (optional) error code that is derived by deserialzing the errorResultXdr string in the sendTransaction response // list of possible errror codes: https://developers.stellar.org/docs/data/horizon/api-reference/errors/result-codes/transactions - Code RPCTXCode + Code RPCTXCode + ErrorResultXDR string } func ParseToRPCSendTxResponse(transactionXDR string, result entities.RPCSendTransactionResult, err error) (RPCSendTxResponse, error) { @@ -134,6 +137,7 @@ func ParseToRPCSendTxResponse(transactionXDR string, result entities.RPCSendTran } sendTxResponse.Status.RPCStatus = result.Status sendTxResponse.TransactionHash = result.Hash + sendTxResponse.ErrorResultXDR = result.ErrorResultXDR sendTxResponse.Code, err = TransactionResultXDRToCode(result.ErrorResultXDR) if err != nil { return sendTxResponse, fmt.Errorf("parse error result xdr string: %w", err) diff --git a/internal/tss/types_test.go b/internal/tss/types_test.go index ed489ff..ad5f0f5 100644 --- a/internal/tss/types_test.go +++ b/internal/tss/types_test.go @@ -26,6 +26,7 @@ func TestParseToRPCSendTxResponse(t *testing.T) { }, nil) assert.Equal(t, entities.PendingStatus, resp.Status.RPCStatus) + assert.Equal(t, "", resp.ErrorResultXDR) assert.Equal(t, EmptyCode, resp.Code.OtherCodes) assert.Empty(t, err) }) @@ -44,6 +45,7 @@ func TestParseToRPCSendTxResponse(t *testing.T) { ErrorResultXDR: "AAAAAAAAAMj////9AAAAAA==", }, nil) + assert.Equal(t, "AAAAAAAAAMj////9AAAAAA==", resp.ErrorResultXDR) assert.Equal(t, xdr.TransactionResultCodeTxTooLate, resp.Code.TxResultCode) assert.Empty(t, err) }) diff --git a/internal/tss/utils/helpers.go b/internal/tss/utils/helpers.go index 94a7fad..dc50d16 100644 --- a/internal/tss/utils/helpers.go +++ b/internal/tss/utils/helpers.go @@ -13,6 +13,7 @@ func PayloadTOTSSResponse(payload tss.Payload) tss.TSSResponse { response.Status = string(payload.RpcSubmitTxResponse.Status.Status()) response.TransactionResultCode = payload.RpcSubmitTxResponse.Code.TxResultCode.String() response.EnvelopeXDR = payload.RpcSubmitTxResponse.TransactionXDR + response.ResultXDR = payload.RpcSubmitTxResponse.ErrorResultXDR } else if payload.RpcGetIngestTxResponse.Status != "" { response.Status = string(payload.RpcGetIngestTxResponse.Status) response.TransactionResultCode = payload.RpcGetIngestTxResponse.Code.TxResultCode.String() From 2f4ae28ea78c0ee421f7823d47263725413c2985 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Thu, 3 Oct 2024 11:03:43 -0700 Subject: [PATCH 099/113] typo --- internal/serve/serve.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/serve/serve.go b/internal/serve/serve.go index 115c780..61fbb3c 100644 --- a/internal/serve/serve.go +++ b/internal/serve/serve.go @@ -290,7 +290,7 @@ func populatePools(poolPopulator tssservices.PoolPopulator) { for range ticker.C { err := poolPopulator.PopulatePools(context.Background()) if err != nil { - log.Ctx(ctx).Error("Ensuring the number of channel accounts in the database...") + log.Ctx(ctx).Error("Populating pools...") } } } From 450fd41b9b7cc8b366d2c6b5cec4fea9fe5c136d Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Thu, 3 Oct 2024 11:06:22 -0700 Subject: [PATCH 100/113] typo --- internal/tss/services/pool_populator.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/tss/services/pool_populator.go b/internal/tss/services/pool_populator.go index fb47fa5..842c091 100644 --- a/internal/tss/services/pool_populator.go +++ b/internal/tss/services/pool_populator.go @@ -191,11 +191,11 @@ func (p *poolPopulator) routeErrorTransactions() error { func (p *poolPopulator) routeFinalTransactions(status tss.RPCTXStatus) error { ctx := context.Background() - failedTxns, err := p.Store.GetTransactionsWithStatus(ctx, status) + finalTxns, err := p.Store.GetTransactionsWithStatus(ctx, status) if err != nil { return fmt.Errorf("unable to get transactions: %w", err) } - for _, txn := range failedTxns { + for _, txn := range finalTxns { payload := tss.Payload{ TransactionHash: txn.Hash, TransactionXDR: txn.XDR, From 44cb3ec6788ee3399f1d0a4cbb35678b6ca85cf9 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Wed, 9 Oct 2024 16:41:14 -0700 Subject: [PATCH 101/113] merge main --- cmd/utils/global_options.go | 166 ------------------------------------ cmd/utils/tss_options.go | 44 ++++++++++ go.sum | 7 -- 3 files changed, 44 insertions(+), 173 deletions(-) diff --git a/cmd/utils/global_options.go b/cmd/utils/global_options.go index 44c0db7..302a79a 100644 --- a/cmd/utils/global_options.go +++ b/cmd/utils/global_options.go @@ -142,172 +142,6 @@ func DistributionAccountSignatureClientProviderOption(configKey *signing.Signatu } } -func RPCURLOption(configKey *string) *config.ConfigOption { - return &config.ConfigOption{ - Name: "rpc-url", - Usage: "The URL of the RPC Server.", - OptType: types.String, - ConfigKey: configKey, - FlagDefault: "localhost:8080", - Required: true, - } -} - -func RPCCallerServiceChannelBufferSizeOption(configKey *int) *config.ConfigOption { - return &config.ConfigOption{ - Name: "tss-rpc-caller-service-channel-buffer-size", - Usage: "Set the buffer size for TSS RPC Caller Service channel.", - OptType: types.Int, - ConfigKey: configKey, - FlagDefault: 1000, - } -} - -func RPCCallerServiceMaxWorkersOption(configKey *int) *config.ConfigOption { - return &config.ConfigOption{ - Name: "tss-rpc-caller-service-channel-max-workers", - Usage: "Set the maximum number of workers for TSS RPC Caller Service channel.", - OptType: types.Int, - ConfigKey: configKey, - FlagDefault: 100, - } - -} - -func ErrorHandlerServiceJitterChannelBufferSizeOption(configKey *int) *config.ConfigOption { - return &config.ConfigOption{ - Name: "error-handler-service-jitter-channel-buffer-size", - Usage: "Set the buffer size of the Error Handler Service Jitter channel.", - OptType: types.Int, - ConfigKey: configKey, - FlagDefault: 100, - Required: true, - } -} - -func ErrorHandlerServiceJitterChannelMaxWorkersOption(configKey *int) *config.ConfigOption { - return &config.ConfigOption{ - Name: "error-handler-service-jitter-channel-max-workers", - Usage: "Set the maximum number of workers for the Error Handler Service Jitter channel.", - OptType: types.Int, - ConfigKey: configKey, - FlagDefault: 10, - Required: true, - } -} - -func ErrorHandlerServiceNonJitterChannelBufferSizeOption(configKey *int) *config.ConfigOption { - return &config.ConfigOption{ - Name: "error-handler-service-non-jitter-channel-buffer-size", - Usage: "Set the buffer size of the Error Handler Service Non Jitter channel.", - OptType: types.Int, - ConfigKey: configKey, - FlagDefault: 100, - Required: true, - } - -} - -func ErrorHandlerServiceNonJitterChannelMaxWorkersOption(configKey *int) *config.ConfigOption { - return &config.ConfigOption{ - Name: "error-handler-service-non-jitter-channel-max-workers", - Usage: "Set the maximum number of workers for the Error Handler Service Non Jitter channel.", - OptType: types.Int, - ConfigKey: configKey, - FlagDefault: 10, - Required: true, - } -} - -func ErrorHandlerServiceJitterChannelMinWaitBtwnRetriesMSOption(configKey *int) *config.ConfigOption { - return &config.ConfigOption{ - Name: "error-handler-service-jitter-channel-min-wait-between-retries", - Usage: "Set the minimum amount of time in ms between retries for the Error Handler Service Jitter channel.", - OptType: types.Int, - ConfigKey: configKey, - FlagDefault: 10, - Required: true, - } -} - -func ErrorHandlerServiceNonJitterChannelWaitBtwnRetriesMSOption(configKey *int) *config.ConfigOption { - return &config.ConfigOption{ - Name: "error-handler-service-non-jitter-channel-wait-between-retries", - Usage: "Set the amount of time in ms between retries for the Error Handler Service Non Jitter channel.", - OptType: types.Int, - ConfigKey: configKey, - FlagDefault: 10, - Required: true, - } -} - -func ErrorHandlerServiceJitterChannelMaxRetriesOptions(configKey *int) *config.ConfigOption { - return &config.ConfigOption{ - Name: "error-handler-service-jitter-channel-max-retries", - Usage: "Set the number of retries for each task in the Error Handler Service Jitter channel.", - OptType: types.Int, - ConfigKey: configKey, - FlagDefault: 10, - Required: true, - } - -} - -func ErrorHandlerServiceNonJitterChannelMaxRetriesOption(configKey *int) *config.ConfigOption { - return &config.ConfigOption{ - Name: "error-handler-service-non-jitter-channel-max-retries", - Usage: "Set the number of retries for each task in the Error Handler Service Non Jitter channel.", - OptType: types.Int, - ConfigKey: configKey, - FlagDefault: 10, - Required: true, - } -} - -func WebhookHandlerServiceChannelMaxBufferSizeOption(configKey *int) *config.ConfigOption { - return &config.ConfigOption{ - Name: "webhook-service-channel-max-buffer-size", - Usage: "Set the buffer size of the webhook serive channel.", - OptType: types.Int, - ConfigKey: configKey, - FlagDefault: 100, - Required: true, - } -} - -func WebhookHandlerServiceChannelMaxWorkersOptions(configKey *int) *config.ConfigOption { - return &config.ConfigOption{ - Name: "webhook-service-channel-max-workers", - Usage: "Set the max number of workers for the webhook serive channel.", - OptType: types.Int, - ConfigKey: configKey, - FlagDefault: 10, - Required: true, - } -} - -func WebhookHandlerServiceChannelMaxRetriesOption(configKey *int) *config.ConfigOption { - return &config.ConfigOption{ - Name: "webhook-service-channel-max-retries", - Usage: "Set the max number of times to ping a webhook before quitting.", - OptType: types.Int, - ConfigKey: configKey, - FlagDefault: 3, - Required: true, - } -} - -func WebhookHandlerServiceChannelMinWaitBtwnRetriesMSOption(configKey *int) *config.ConfigOption { - return &config.ConfigOption{ - Name: "webhook-service-channel-min-wait-between-retries", - Usage: "The minumum amout of time to wait before repining the webhook url", - OptType: types.Int, - ConfigKey: configKey, - FlagDefault: 10, - Required: true, - } -} - func AWSOptions(awsRegionConfigKey *string, kmsKeyARN *string, required bool) config.ConfigOptions { awsOpts := config.ConfigOptions{ { diff --git a/cmd/utils/tss_options.go b/cmd/utils/tss_options.go index 852ee80..460216a 100644 --- a/cmd/utils/tss_options.go +++ b/cmd/utils/tss_options.go @@ -116,3 +116,47 @@ func ErrorHandlerServiceNonJitterChannelMaxRetriesOption(configKey *int) *config Required: true, } } + +func WebhookHandlerServiceChannelMaxBufferSizeOption(configKey *int) *config.ConfigOption { + return &config.ConfigOption{ + Name: "webhook-service-channel-max-buffer-size", + Usage: "Set the buffer size of the webhook serive channel.", + OptType: types.Int, + ConfigKey: configKey, + FlagDefault: 100, + Required: true, + } +} + +func WebhookHandlerServiceChannelMaxWorkersOptions(configKey *int) *config.ConfigOption { + return &config.ConfigOption{ + Name: "webhook-service-channel-max-workers", + Usage: "Set the max number of workers for the webhook serive channel.", + OptType: types.Int, + ConfigKey: configKey, + FlagDefault: 10, + Required: true, + } +} + +func WebhookHandlerServiceChannelMaxRetriesOption(configKey *int) *config.ConfigOption { + return &config.ConfigOption{ + Name: "webhook-service-channel-max-retries", + Usage: "Set the max number of times to ping a webhook before quitting.", + OptType: types.Int, + ConfigKey: configKey, + FlagDefault: 3, + Required: true, + } +} + +func WebhookHandlerServiceChannelMinWaitBtwnRetriesMSOption(configKey *int) *config.ConfigOption { + return &config.ConfigOption{ + Name: "webhook-service-channel-min-wait-between-retries", + Usage: "The minumum amout of time to wait before repining the webhook url", + OptType: types.Int, + ConfigKey: configKey, + FlagDefault: 10, + Required: true, + } +} diff --git a/go.sum b/go.sum index 782d14b..1ea40b5 100644 --- a/go.sum +++ b/go.sum @@ -409,10 +409,3 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= - From 61cf2dbf099b27d15f9b120a6a6f70a58898264a Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Wed, 9 Oct 2024 17:00:26 -0700 Subject: [PATCH 102/113] merged latest --- cmd/utils/global_options.go | 11 -- go.mod | 31 ---- go.sum | 321 ------------------------------------ 3 files changed, 363 deletions(-) diff --git a/cmd/utils/global_options.go b/cmd/utils/global_options.go index 12c9a0d..86e6847 100644 --- a/cmd/utils/global_options.go +++ b/cmd/utils/global_options.go @@ -142,17 +142,6 @@ func DistributionAccountSignatureClientProviderOption(configKey *signing.Signatu } } -func RPCURLOption(configKey *string) *config.ConfigOption { - return &config.ConfigOption{ - Name: "rpc-url", - Usage: "The URL of the RPC Server.", - OptType: types.String, - ConfigKey: configKey, - FlagDefault: "localhost:8080", - Required: true, - } -} - func StartLedgerOption(configKey *int) *config.ConfigOption { return &config.ConfigOption{ Name: "start-ledger", diff --git a/go.mod b/go.mod index 5518b0e..a7e936e 100644 --- a/go.mod +++ b/go.mod @@ -23,34 +23,20 @@ require ( ) require ( - cloud.google.com/go v0.112.1 // indirect - cloud.google.com/go/compute v1.24.0 // indirect - cloud.google.com/go/compute/metadata v0.2.3 // indirect - cloud.google.com/go/iam v1.1.6 // indirect - cloud.google.com/go/storage v1.38.0 // indirect github.com/BurntSushi/toml v1.3.2 // indirect github.com/Masterminds/squirrel v1.5.4 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/djherbis/fscache v0.10.1 // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/go-errors/errors v1.5.1 // indirect github.com/go-gorp/gorp/v3 v3.1.0 // indirect - github.com/go-logr/logr v1.4.1 // indirect - github.com/go-logr/stdr v1.2.2 // indirect - github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/golang/protobuf v1.5.3 // indirect - github.com/google/s2a-go v0.1.7 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect - github.com/googleapis/gax-go/v2 v2.12.3 // indirect github.com/gorilla/schema v1.4.1 // indirect - github.com/hashicorp/golang-lru v1.0.2 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect @@ -61,7 +47,6 @@ require ( github.com/manucorporat/sse v0.0.0-20160126180136-ee05b128a739 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect @@ -79,27 +64,11 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect - go.opencensus.io v0.24.0 // indirect - go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 // indirect - go.opentelemetry.io/otel v1.24.0 // indirect - go.opentelemetry.io/otel/metric v1.24.0 // indirect - go.opentelemetry.io/otel/trace v1.24.0 // indirect go.uber.org/multierr v1.11.0 // indirect golang.org/x/crypto v0.21.0 // indirect golang.org/x/net v0.23.0 // indirect - golang.org/x/oauth2 v0.18.0 // indirect - golang.org/x/sync v0.6.0 // indirect golang.org/x/sys v0.26.0 // indirect golang.org/x/text v0.14.0 // indirect - golang.org/x/time v0.5.0 // indirect - golang.org/x/tools v0.14.0 // indirect - google.golang.org/api v0.171.0 // indirect - google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20240311132316-a219d84964c2 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c // indirect - google.golang.org/grpc v1.62.1 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/tylerb/graceful.v1 v1.2.15 // indirect diff --git a/go.sum b/go.sum index f390311..09edff9 100644 --- a/go.sum +++ b/go.sum @@ -1,54 +1,5 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= -cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -cloud.google.com/go v0.112.1 h1:uJSeirPke5UNZHIb4SxfZklVSiWWVqW4oXlETwZziwM= -cloud.google.com/go v0.112.1/go.mod h1:+Vbu+Y1UU+I1rjmzeMOb/8RfkKJK2Gyxi1X6jJCZLo4= -cloud.google.com/go/compute v1.24.0 h1:phWcR2eWzRJaL/kOiJwfFsPs4BaKq1j6vnpZrc1YlVg= -cloud.google.com/go/compute v1.24.0/go.mod h1:kw1/T+h/+tK2LJK0wiPPx1intgdAM3j/g3hFDlscY40= -cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= -cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= -cloud.google.com/go/iam v1.1.6 h1:bEa06k05IO4f4uJonbB5iAgKTPpABy1ayxaIZV/GHVc= -cloud.google.com/go/iam v1.1.6/go.mod h1:O0zxdPeGBoFdWW3HWmBxJsk0pfvNM/p/qa82rWOGTwI= -cloud.google.com/go/storage v1.38.0 h1:Az68ZRGlnNTpIBbLjSMIV2BDcwwXYlRlQzis0llkpJg= -cloud.google.com/go/storage v1.38.0/go.mod h1:tlUADB0mAb9BgYls9lq+8MGkfzOXuLrnHXlpHmvFJoY= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= @@ -65,31 +16,15 @@ github.com/aws/aws-sdk-go v1.55.5 h1:KKUZBfBoyqy5d3swXyiC7Q76ic40rYcbqH7qjh59kzU github.com/aws/aws-sdk-go v1.55.5/go.mod h1:eRwEWoyTWFMVYVQzKMNHWP5/RV4xIUGMQfXQHfHkpNU= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 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= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/structs v1.1.0 h1:Q7juDM0QtcnhCpeyLGQKyg4TOIghuNXrkL32pHAUMxo= github.com/fatih/structs v1.1.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= -github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= -github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= -github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= -github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= @@ -106,11 +41,6 @@ github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8b github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= -github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= @@ -121,78 +51,21 @@ github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27 github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/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= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -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.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -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= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= -github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc= -github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= -github.com/guregu/null v4.0.0+incompatible h1:4zw0ckM7ECd6FNNddc3Fu4aty9nTlpkkzH7dPn4/4Gw= -github.com/guregu/null v4.0.0+incompatible/go.mod h1:ePGpQaN9cw0tj45IR5E5ehMvsFlLlQZAkkOXZurJ3NM= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw= -github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= -github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= -github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= -github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= -github.com/googleapis/gax-go/v2 v2.12.3 h1:5/zPPDvw8Q1SuXjrqrZslrqT7dL/uJT2CQii/cLCKqA= -github.com/googleapis/gax-go/v2 v2.12.3/go.mod h1:AKloxT6GtNbaLm8QTNSidHUVsHYcBHwWRvkNFJUQcS4= github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E= github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM= github.com/guregu/null v4.0.0+incompatible h1:4zw0ckM7ECd6FNNddc3Fu4aty9nTlpkkzH7dPn4/4Gw= github.com/guregu/null v4.0.0+incompatible/go.mod h1:ePGpQaN9cw0tj45IR5E5ehMvsFlLlQZAkkOXZurJ3NM= -github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= -github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= @@ -239,10 +112,6 @@ github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= -github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= -github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= -github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= -github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= @@ -256,7 +125,6 @@ github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY= github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg= github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= @@ -328,199 +196,23 @@ github.com/yudai/gojsondiff v1.0.0 h1:27cbfqXLVEJ1o8I6v3y9lg8Ydm53EKqHXAOMxEGlCO github.com/yudai/gojsondiff v1.0.0/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82 h1:BHyfKlQyqbsFN5p3IfnEUduWvb9is428/nNb5L3U01M= github.com/yudai/golcs v0.0.0-20170316035057-ecda9a501e82/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg= -go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0/go.mod h1:Mjt1i1INqiaoZOMGR1RIUJN+i3ChKoFRqzrRQhlkbs0= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0 h1:jq9TW8u3so/bN+JPT166wjOI6/vQPF6Xe7nMNIltagk= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.49.0/go.mod h1:p8pYQP+m5XfbZm9fxtSKAbM6oIllS7s2AfxrChvc7iw= -go.opentelemetry.io/otel v1.24.0 h1:0LAOdjNmQeSTzGBzduGe/rU4tZhMwL5rWgtp9Ku5Jfo= -go.opentelemetry.io/otel v1.24.0/go.mod h1:W7b9Ozg4nkF5tWI5zsXkaKKDjdVjpD4oAt9Qi/MArHo= -go.opentelemetry.io/otel/metric v1.24.0 h1:6EhoGWWK28x1fbpA4tYTOWBkPefTDQnb8WSGXlc88kI= -go.opentelemetry.io/otel/metric v1.24.0/go.mod h1:VYhLe1rFfxuTXLgj4CBiyz+9WYBA8pNGJgDcSFRKBco= -go.opentelemetry.io/otel/sdk v1.22.0 h1:6coWHw9xw7EfClIC/+O31R8IY3/+EiRFHevmHafB2Gw= -go.opentelemetry.io/otel/sdk v1.22.0/go.mod h1:iu7luyVGYovrRpe2fmj3CVKouQNdTOkxtLzPvPz1DOc= -go.opentelemetry.io/otel/trace v1.24.0 h1:CsKnnL4dUAr/0llH9FKuc698G04IrpWV0MQA/Y1YELI= -go.opentelemetry.io/otel/trace v1.24.0/go.mod h1:HPc3Xr/cOApsBI154IU0OI0HJexz+aw5uPdbs3UCjNU= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI= -golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24= golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= -google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= -google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU= -golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= -google.golang.org/api v0.171.0 h1:w174hnBPqut76FzW5Qaupt7zY8Kql6fiVjgys4f58sU= -google.golang.org/api v0.171.0/go.mod h1:Hnq5AHm4OTMt2BUVjael2CWZFD6vksJdWCWiUAmjC9o= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= -google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y= -google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:mqHbVIp48Muh7Ywss/AD6I5kNVKZMmAa/QEW58Gxp2s= -google.golang.org/genproto/googleapis/api v0.0.0-20240311132316-a219d84964c2 h1:rIo7ocm2roD9DcFIX67Ym8icoGCKSARAiPljFhh5suQ= -google.golang.org/genproto/googleapis/api v0.0.0-20240311132316-a219d84964c2/go.mod h1:O1cOfN1Cy6QEYr7VxtjOyP5AdAuR0aJ/MYZaaof623Y= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c h1:lfpJ/2rWPa/kJgxyyXM8PrNnfCzcmxJ265mADgwmvLI= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= -google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk= -google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= @@ -528,11 +220,6 @@ google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHh gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/djherbis/atime.v1 v1.0.0 h1:eMRqB/JrLKocla2PBPKgQYg/p5UG4L6AUAs92aP7F60= -gopkg.in/djherbis/atime.v1 v1.0.0/go.mod h1:hQIUStKmJfvf7xdh/wtK84qe+DsTV5LnA9lzxxtPpJ8= -gopkg.in/djherbis/stream.v1 v1.3.1 h1:uGfmsOY1qqMjQQphhRBSGLyA9qumJ56exkRu9ASTjCw= -gopkg.in/djherbis/stream.v1 v1.3.1/go.mod h1:aEV8CBVRmSpLamVJfM903Npic1IKmb2qS30VAZ+sssg= gopkg.in/gavv/httpexpect.v1 v1.0.0-20170111145843-40724cf1e4a0 h1:r5ptJ1tBxVAeqw4CrYWhXIMr0SybY3CDHuIbCg5CFVw= gopkg.in/gavv/httpexpect.v1 v1.0.0-20170111145843-40724cf1e4a0/go.mod h1:WtiW9ZA1LdaWqtQRo1VbIL/v4XZ8NDta+O/kSpGgVek= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= @@ -547,11 +234,3 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= From ad392d82f098d46bd06fee7fe1d084d6f08a32a8 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Tue, 15 Oct 2024 11:45:41 -0400 Subject: [PATCH 103/113] typo in desc string --- cmd/utils/tss_options.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/utils/tss_options.go b/cmd/utils/tss_options.go index 460216a..7a3e9b0 100644 --- a/cmd/utils/tss_options.go +++ b/cmd/utils/tss_options.go @@ -153,7 +153,7 @@ func WebhookHandlerServiceChannelMaxRetriesOption(configKey *int) *config.Config func WebhookHandlerServiceChannelMinWaitBtwnRetriesMSOption(configKey *int) *config.ConfigOption { return &config.ConfigOption{ Name: "webhook-service-channel-min-wait-between-retries", - Usage: "The minumum amout of time to wait before repining the webhook url", + Usage: "The minumum amout of time to wait before resending the payload to the webhook url", OptType: types.Int, ConfigKey: configKey, FlagDefault: 10, From dbe96290633550b8c7a1ed1ca7dee908696e9c25 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Tue, 15 Oct 2024 11:47:05 -0400 Subject: [PATCH 104/113] millisecond wait time --- internal/tss/channels/webhook_channel.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/tss/channels/webhook_channel.go b/internal/tss/channels/webhook_channel.go index cc13790..0e8e637 100644 --- a/internal/tss/channels/webhook_channel.go +++ b/internal/tss/channels/webhook_channel.go @@ -68,7 +68,7 @@ func (p *webhookPool) Receive(payload tss.Payload) { return } currentBackoff := p.MinWaitBtwnRetriesMS * (1 << i) - time.Sleep(jitter(time.Duration(currentBackoff)) * time.Microsecond) + time.Sleep(jitter(time.Duration(currentBackoff)) * time.Millisecond) } } From 0957985b6c7abc22842b52985b0a7b4af812302b Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Tue, 15 Oct 2024 15:29:32 -0400 Subject: [PATCH 105/113] changes based on comments --- cmd/ingest.go | 1 - internal/services/ingest.go | 1 - internal/services/ingest_test.go | 6 +++--- internal/services/rpc_service.go | 2 +- internal/tss/router/router.go | 2 +- internal/tss/router/router_test.go | 4 ++-- internal/tss/types.go | 2 +- internal/utils/ingestion_utils.go | 1 - 8 files changed, 8 insertions(+), 11 deletions(-) diff --git a/cmd/ingest.go b/cmd/ingest.go index 6c47706..32dab4d 100644 --- a/cmd/ingest.go +++ b/cmd/ingest.go @@ -56,7 +56,6 @@ func (c *ingestCmd) Command() *cobra.Command { Use: "ingest", Short: "Run Ingestion service", PersistentPreRunE: func(cmd *cobra.Command, args []string) error { - // SET UP WEBHOOK CHANNEL HERE if err := cfgOpts.RequireE(); err != nil { return fmt.Errorf("requiring values of config options: %w", err) } diff --git a/internal/services/ingest.go b/internal/services/ingest.go index a432cf1..43ccf30 100644 --- a/internal/services/ingest.go +++ b/internal/services/ingest.go @@ -152,7 +152,6 @@ func (m *ingestService) ingestPayments(ctx context.Context, ledgerTransactions [ if txMemo != nil { *txMemo = utils.SanitizeUTF8(*txMemo) } - txEnvelopeXDR.SourceAccount() for idx, op := range txEnvelopeXDR.Operations() { opIdx := idx + 1 diff --git a/internal/services/ingest_test.go b/internal/services/ingest_test.go index 7405773..5548d88 100644 --- a/internal/services/ingest_test.go +++ b/internal/services/ingest_test.go @@ -55,7 +55,7 @@ func TestGetLedgerTransactions(t *testing.T) { txns, err := ingestService.GetLedgerTransactions(1) assert.Equal(t, 1, len(txns)) assert.Equal(t, txns[0].Hash, "hash1") - assert.Empty(t, err) + assert.NoError(t, err) }) t.Run("ledger_transactions_split_between_multiple_gettransactions_calls", func(t *testing.T) { @@ -105,7 +105,7 @@ func TestGetLedgerTransactions(t *testing.T) { assert.Equal(t, txns[0].Hash, "hash1") assert.Equal(t, txns[1].Hash, "hash2") assert.Equal(t, txns[2].Hash, "hash3") - assert.Empty(t, err) + assert.NoError(t, err) }) } @@ -150,7 +150,7 @@ func TestProcessTSSTransactions(t *testing.T) { Once() err := ingestService.processTSSTransactions(context.Background(), transactions) - assert.Empty(t, err) + assert.NoError(t, err) updatedTX, _ := tssStore.GetTransaction(context.Background(), "hash") assert.Equal(t, string(entities.SuccessStatus), updatedTX.Status) diff --git a/internal/services/rpc_service.go b/internal/services/rpc_service.go index 1b9dce2..62c4601 100644 --- a/internal/services/rpc_service.go +++ b/internal/services/rpc_service.go @@ -58,7 +58,7 @@ func (r *rpcService) GetTransaction(transactionHash string) (entities.RPCGetTran func (r *rpcService) GetTransactions(startLedger int64, startCursor string, limit int) (entities.RPCGetTransactionsResult, error) { if limit > PageLimit { - return entities.RPCGetTransactionsResult{}, fmt.Errorf("limit cannot exceed") + return entities.RPCGetTransactionsResult{}, fmt.Errorf("limit cannot exceed %d", PageLimit) } params := entities.RPCParams{} if startCursor != "" { diff --git a/internal/tss/router/router.go b/internal/tss/router/router.go index b974203..d548349 100644 --- a/internal/tss/router/router.go +++ b/internal/tss/router/router.go @@ -51,7 +51,7 @@ func (r *router) Route(payload tss.Payload) error { channel = r.ErrorJitterChannel } else if slices.Contains(tss.NonJitterErrorCodes, payload.RpcSubmitTxResponse.Code.TxResultCode) { channel = r.ErrorNonJitterChannel - } else if slices.Contains(tss.FinalErrorCodes, payload.RpcSubmitTxResponse.Code.TxResultCode) { + } else if slices.Contains(tss.FinalCodes, payload.RpcSubmitTxResponse.Code.TxResultCode) { channel = r.WebhookChannel } } diff --git a/internal/tss/router/router_test.go b/internal/tss/router/router_test.go index 11bbe4c..9156c01 100644 --- a/internal/tss/router/router_test.go +++ b/internal/tss/router/router_test.go @@ -97,7 +97,7 @@ func TestRouter(t *testing.T) { } }) t.Run("status_error_routes_to_webhook_channel", func(t *testing.T) { - for _, code := range tss.FinalErrorCodes { + for _, code := range tss.FinalCodes { payload := tss.Payload{ RpcSubmitTxResponse: tss.RPCSendTxResponse{ Status: tss.RPCTXStatus{ @@ -123,7 +123,7 @@ func TestRouter(t *testing.T) { RpcGetIngestTxResponse: tss.RPCGetIngestTxResponse{ Status: entities.SuccessStatus, Code: tss.RPCTXCode{ - TxResultCode: tss.FinalErrorCodes[0], + TxResultCode: tss.FinalCodes[0], }, }, } diff --git a/internal/tss/types.go b/internal/tss/types.go index b10876d..5071ec8 100644 --- a/internal/tss/types.go +++ b/internal/tss/types.go @@ -93,7 +93,7 @@ func (c RPCTXCode) Code() int { return int(c.TxResultCode) } -var FinalErrorCodes = []xdr.TransactionResultCode{ +var FinalCodes = []xdr.TransactionResultCode{ xdr.TransactionResultCodeTxSuccess, xdr.TransactionResultCodeTxFailed, xdr.TransactionResultCodeTxMissingOperation, diff --git a/internal/utils/ingestion_utils.go b/internal/utils/ingestion_utils.go index 1235ce8..b2b5225 100644 --- a/internal/utils/ingestion_utils.go +++ b/internal/utils/ingestion_utils.go @@ -62,7 +62,6 @@ func SourceAccount(op xdr.Operation, txEnvelope xdr.TransactionEnvelope) string if account != nil { return account.ToAccountId().Address() } - txEnvelope.SourceAccount() return txEnvelope.SourceAccount().ToAccountId().Address() } From c889a86cba533b60613a18a782ecc9db88886d6f Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Tue, 15 Oct 2024 16:16:41 -0400 Subject: [PATCH 106/113] typo --- internal/tss/services/pool_populator.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/tss/services/pool_populator.go b/internal/tss/services/pool_populator.go index 842c091..c11f156 100644 --- a/internal/tss/services/pool_populator.go +++ b/internal/tss/services/pool_populator.go @@ -151,7 +151,7 @@ func (p *poolPopulator) routeErrorTransactions() error { if err != nil { return fmt.Errorf("gretting latest try for transaction: %w", err) } - if slices.Contains(tss.FinalErrorCodes, xdr.TransactionResultCode(try.Code)) { + if slices.Contains(tss.FinalCodes, xdr.TransactionResultCode(try.Code)) { // route to webhook channel payload.RpcSubmitTxResponse = tss.RPCSendTxResponse{ TransactionHash: try.Hash, From eb49994a79f09ac470a364546f1b019315aca5d1 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Wed, 16 Oct 2024 12:36:32 -0400 Subject: [PATCH 107/113] changes based on comments --- .../2024-08-28.0-tss_transactions.sql | 4 +--- ...4-10-14.0-alter_tss_transactiion_tries.sql | 15 ++++++++++++ internal/serve/serve.go | 2 +- internal/tss/channels/webhook_channel.go | 2 -- internal/tss/channels/webhook_channel_test.go | 3 ++- internal/tss/router/router_test.go | 3 ++- internal/tss/services/pool_populator.go | 24 +++++++------------ internal/tss/services/pool_populator_test.go | 14 +++++------ internal/tss/store/store.go | 1 - 9 files changed, 37 insertions(+), 31 deletions(-) create mode 100644 internal/db/migrations/2024-10-14.0-alter_tss_transactiion_tries.sql diff --git a/internal/db/migrations/2024-08-28.0-tss_transactions.sql b/internal/db/migrations/2024-08-28.0-tss_transactions.sql index 500c550..9eba501 100644 --- a/internal/db/migrations/2024-08-28.0-tss_transactions.sql +++ b/internal/db/migrations/2024-08-28.0-tss_transactions.sql @@ -14,9 +14,7 @@ CREATE TABLE tss_transaction_submission_tries ( try_transaction_hash TEXT PRIMARY KEY, original_transaction_hash TEXT NOT NULL, try_transaction_xdr TEXT NOT NULL, - status TEXT NOT NULL, - code INTEGER NOT NULL, - result_xdr TEXT NOT NULL, + status INTEGER NOT NULL, updated_at TIMESTAMPTZ DEFAULT NOW() NOT NULL ); diff --git a/internal/db/migrations/2024-10-14.0-alter_tss_transactiion_tries.sql b/internal/db/migrations/2024-10-14.0-alter_tss_transactiion_tries.sql new file mode 100644 index 0000000..f99cd1f --- /dev/null +++ b/internal/db/migrations/2024-10-14.0-alter_tss_transactiion_tries.sql @@ -0,0 +1,15 @@ +-- +migrate Up + +ALTER TABLE tss_transaction_submission_tries + ALTER COLUMN status TYPE TEXT, + ALTER COLUMN status SET NOT NULL, + ADD COLUMN code INTEGER NOT NULL, + ADD COLUMN result_xdr TEXT NOT NULL; + +-- +migrate Down + +ALTER TABLE tss_transaction_submission_tries + ALTER COLUMN status TYPE INTEGER, + ALTER COLUMN status SET NOT NULL, + DROP COLUMN code, + DROP COLUMN result_xdr; diff --git a/internal/serve/serve.go b/internal/serve/serve.go index a97e7b0..57be014 100644 --- a/internal/serve/serve.go +++ b/internal/serve/serve.go @@ -289,7 +289,7 @@ func populatePools(poolPopulator tssservices.PoolPopulator) { ctx := context.Background() for range ticker.C { - err := poolPopulator.PopulatePools(context.Background()) + err := poolPopulator.PopulatePools(ctx) if err != nil { log.Ctx(ctx).Error("Populating pools...") } diff --git a/internal/tss/channels/webhook_channel.go b/internal/tss/channels/webhook_channel.go index da3a3df..e844f42 100644 --- a/internal/tss/channels/webhook_channel.go +++ b/internal/tss/channels/webhook_channel.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "encoding/json" - "fmt" "net/http" "time" @@ -77,7 +76,6 @@ func (p *webhookPool) Receive(payload tss.Payload) { err := p.Store.UpsertTransaction( ctx, payload.WebhookURL, payload.TransactionHash, payload.TransactionXDR, tss.RPCTXStatus{OtherStatus: tss.SentStatus}) if err != nil { - fmt.Println(err) log.Errorf("%s: error updating transaction status: %e", WebhookChannelName, err) } break diff --git a/internal/tss/channels/webhook_channel_test.go b/internal/tss/channels/webhook_channel_test.go index 29a9b69..fe20572 100644 --- a/internal/tss/channels/webhook_channel_test.go +++ b/internal/tss/channels/webhook_channel_test.go @@ -68,6 +68,7 @@ func TestWebhookHandlerServiceChannel(t *testing.T) { mockHTTPClient.AssertNumberOfCalls(t, "Post", 2) - tx, _ := store.GetTransaction(context.Background(), payload.TransactionHash) + tx, err := store.GetTransaction(context.Background(), payload.TransactionHash) assert.Equal(t, string(tss.SentStatus), tx.Status) + assert.NoError(t, err) } diff --git a/internal/tss/router/router_test.go b/internal/tss/router/router_test.go index fc0b3a5..cbf5200 100644 --- a/internal/tss/router/router_test.go +++ b/internal/tss/router/router_test.go @@ -161,7 +161,8 @@ func TestRouter(t *testing.T) { Return(). Once() - _ = router.Route(payload) + err := router.Route(payload) + assert.NoError(t, err) webhookChannel.AssertCalled(t, "Send", payload) }) diff --git a/internal/tss/services/pool_populator.go b/internal/tss/services/pool_populator.go index c11f156..bb948c3 100644 --- a/internal/tss/services/pool_populator.go +++ b/internal/tss/services/pool_populator.go @@ -43,36 +43,34 @@ func NewPoolPopulator(router router.Router, store store.Store, rpcService servic } func (p *poolPopulator) PopulatePools(ctx context.Context) error { - - err := p.routeNewTransactions() + err := p.routeNewTransactions(ctx) if err != nil { return fmt.Errorf("error routing new transactions: %w", err) } - err = p.routeErrorTransactions() + err = p.routeErrorTransactions(ctx) if err != nil { return fmt.Errorf("error routing new transactions: %w", err) } - err = p.routeFinalTransactions(tss.RPCTXStatus{RPCStatus: entities.FailedStatus}) + err = p.routeFinalTransactions(ctx, tss.RPCTXStatus{RPCStatus: entities.FailedStatus}) if err != nil { return fmt.Errorf("error routing failed transactions: %w", err) } - err = p.routeFinalTransactions(tss.RPCTXStatus{RPCStatus: entities.SuccessStatus}) + err = p.routeFinalTransactions(ctx, tss.RPCTXStatus{RPCStatus: entities.SuccessStatus}) if err != nil { return fmt.Errorf("error routing successful transactions: %w", err) } - err = p.routeNotSentTransactions() + err = p.routeNotSentTransactions(ctx) if err != nil { return fmt.Errorf("error routing not_sent transactions: %w", err) } return nil } -func (p *poolPopulator) routeNewTransactions() error { - ctx := context.Background() +func (p *poolPopulator) routeNewTransactions(ctx context.Context) error { newTxns, err := p.Store.GetTransactionsWithStatus(ctx, tss.RPCTXStatus{OtherStatus: tss.NewStatus}) if err != nil { return fmt.Errorf("unable to get transactions: %w", err) @@ -106,7 +104,6 @@ func (p *poolPopulator) routeNewTransactions() error { if getTransactionResult.Status == entities.NotFoundStatus { genericTx, err := txnbuild.TransactionFromXDR(try.XDR) if err != nil { - fmt.Println(txn.XDR) return fmt.Errorf("unmarshaling tx from xdr string: %w", err) } feeBumpTx, unpackable := genericTx.FeeBump() @@ -135,8 +132,7 @@ func (p *poolPopulator) routeNewTransactions() error { return nil } -func (p *poolPopulator) routeErrorTransactions() error { - ctx := context.Background() +func (p *poolPopulator) routeErrorTransactions(ctx context.Context) error { errorTxns, err := p.Store.GetTransactionsWithStatus(ctx, tss.RPCTXStatus{RPCStatus: entities.ErrorStatus}) if err != nil { return fmt.Errorf("unable to get transactions: %w", err) @@ -189,8 +185,7 @@ func (p *poolPopulator) routeErrorTransactions() error { return nil } -func (p *poolPopulator) routeFinalTransactions(status tss.RPCTXStatus) error { - ctx := context.Background() +func (p *poolPopulator) routeFinalTransactions(ctx context.Context, status tss.RPCTXStatus) error { finalTxns, err := p.Store.GetTransactionsWithStatus(ctx, status) if err != nil { return fmt.Errorf("unable to get transactions: %w", err) @@ -219,8 +214,7 @@ func (p *poolPopulator) routeFinalTransactions(status tss.RPCTXStatus) error { return nil } -func (p *poolPopulator) routeNotSentTransactions() error { - ctx := context.Background() +func (p *poolPopulator) routeNotSentTransactions(ctx context.Context) error { notSentTxns, err := p.Store.GetTransactionsWithStatus(ctx, tss.RPCTXStatus{OtherStatus: tss.NotSentStatus}) if err != nil { return fmt.Errorf("unable to get transactions: %w", err) diff --git a/internal/tss/services/pool_populator_test.go b/internal/tss/services/pool_populator_test.go index b9c88c7..4304dc9 100644 --- a/internal/tss/services/pool_populator_test.go +++ b/internal/tss/services/pool_populator_test.go @@ -42,7 +42,7 @@ func TestRouteNewTransactions(t *testing.T) { Return(nil). Once() - err := populator.routeNewTransactions() + err := populator.routeNewTransactions(context.Background()) assert.Empty(t, err) }) @@ -74,7 +74,7 @@ func TestRouteNewTransactions(t *testing.T) { Return(nil). Once() - err := populator.routeNewTransactions() + err := populator.routeNewTransactions(context.Background()) assert.Empty(t, err) }) @@ -94,7 +94,7 @@ func TestRouteNewTransactions(t *testing.T) { Return(rpcGetTransacrionResp, nil). Once() - err := populator.routeNewTransactions() + err := populator.routeNewTransactions(context.Background()) assert.Empty(t, err) }) } @@ -133,7 +133,7 @@ func TestRouteErrorTransactions(t *testing.T) { Return(nil). Once() - err := populator.routeErrorTransactions() + err := populator.routeErrorTransactions(context.Background()) assert.Empty(t, err) }) t.Run("tx_timebounds_not_exceeded", func(t *testing.T) { @@ -142,7 +142,7 @@ func TestRouteErrorTransactions(t *testing.T) { _ = store.UpsertTransaction(context.Background(), "localhost:8000/webhook", "hash", "xdr", tss.RPCTXStatus{RPCStatus: entities.ErrorStatus}) _ = store.UpsertTry(context.Background(), "hash", "feebumphash", txXDRStr, tss.RPCTXStatus{RPCStatus: entities.ErrorStatus}, tss.RPCTXCode{OtherCodes: tss.RPCFailCode}, "ABCD") - err := populator.routeErrorTransactions() + err := populator.routeErrorTransactions(context.Background()) assert.Empty(t, err) }) } @@ -180,7 +180,7 @@ func TestRouteFinalTransactions(t *testing.T) { Return(nil). Once() - err = populator.routeFinalTransactions(tss.RPCTXStatus{RPCStatus: entities.SuccessStatus}) + err = populator.routeFinalTransactions(context.Background(), tss.RPCTXStatus{RPCStatus: entities.SuccessStatus}) assert.Empty(t, err) }) } @@ -219,7 +219,7 @@ func TestNotSentTransactions(t *testing.T) { Return(nil). Once() - err = populator.routeNotSentTransactions() + err = populator.routeNotSentTransactions(context.Background()) assert.Empty(t, err) }) } diff --git a/internal/tss/store/store.go b/internal/tss/store/store.go index 42906c3..032dbd6 100644 --- a/internal/tss/store/store.go +++ b/internal/tss/store/store.go @@ -104,7 +104,6 @@ func (s *store) GetTransaction(ctx context.Context, hash string) (Transaction, e err := s.DB.GetContext(ctx, &transaction, q, hash) if err != nil { if errors.Is(err, sql.ErrNoRows) { - fmt.Println("empty") return Transaction{}, nil } return Transaction{}, fmt.Errorf("getting transaction: %w", err) From f50ba231388b93fd6876a7965e8f1122ce910ead Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Wed, 16 Oct 2024 13:29:34 -0400 Subject: [PATCH 108/113] fixing migrations --- ...ql => 2024-10-14.0-alter_tss_transaction_tries.sql} | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) rename internal/db/migrations/{2024-10-14.0-alter_tss_transactiion_tries.sql => 2024-10-14.0-alter_tss_transaction_tries.sql} (56%) diff --git a/internal/db/migrations/2024-10-14.0-alter_tss_transactiion_tries.sql b/internal/db/migrations/2024-10-14.0-alter_tss_transaction_tries.sql similarity index 56% rename from internal/db/migrations/2024-10-14.0-alter_tss_transactiion_tries.sql rename to internal/db/migrations/2024-10-14.0-alter_tss_transaction_tries.sql index f99cd1f..4828f6e 100644 --- a/internal/db/migrations/2024-10-14.0-alter_tss_transactiion_tries.sql +++ b/internal/db/migrations/2024-10-14.0-alter_tss_transaction_tries.sql @@ -1,15 +1,15 @@ -- +migrate Up ALTER TABLE tss_transaction_submission_tries - ALTER COLUMN status TYPE TEXT, - ALTER COLUMN status SET NOT NULL, + DROP column status, + ADD column status TEXT NOT NULL, ADD COLUMN code INTEGER NOT NULL, ADD COLUMN result_xdr TEXT NOT NULL; -- +migrate Down ALTER TABLE tss_transaction_submission_tries - ALTER COLUMN status TYPE INTEGER, - ALTER COLUMN status SET NOT NULL, + DROP column status, DROP COLUMN code, - DROP COLUMN result_xdr; + DROP COLUMN result_xdr, + ADD column status INTEGER NOT NULL; From 01f2d49b2abb9b6700c0c50d4d2a1883135464df Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Wed, 16 Oct 2024 13:43:16 -0400 Subject: [PATCH 109/113] compare types directly on route --- internal/tss/router/router.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/tss/router/router.go b/internal/tss/router/router.go index aa880f2..ef45e76 100644 --- a/internal/tss/router/router.go +++ b/internal/tss/router/router.go @@ -40,12 +40,12 @@ func NewRouter(cfg RouterConfigs) Router { func (r *router) Route(payload tss.Payload) error { var channel tss.Channel if payload.RpcSubmitTxResponse.Status.Status() != "" { - switch payload.RpcSubmitTxResponse.Status.Status() { - case string(tss.NewStatus): + switch payload.RpcSubmitTxResponse.Status { + case tss.RPCTXStatus{OtherStatus: tss.NewStatus}: channel = r.RPCCallerChannel - case string(entities.TryAgainLaterStatus): + case tss.RPCTXStatus{RPCStatus: entities.TryAgainLaterStatus}: channel = r.ErrorJitterChannel - case string(entities.ErrorStatus): + case tss.RPCTXStatus{RPCStatus: entities.ErrorStatus}: if payload.RpcSubmitTxResponse.Code.OtherCodes == tss.NoCode { if slices.Contains(tss.JitterErrorCodes, payload.RpcSubmitTxResponse.Code.TxResultCode) { channel = r.ErrorJitterChannel @@ -55,9 +55,9 @@ func (r *router) Route(payload tss.Payload) error { channel = r.WebhookChannel } } - case string(entities.SuccessStatus): + case tss.RPCTXStatus{RPCStatus: entities.SuccessStatus}: channel = r.WebhookChannel - case string(entities.FailedStatus): + case tss.RPCTXStatus{RPCStatus: entities.FailedStatus}: channel = r.WebhookChannel default: // Do nothing for PENDING / DUPLICATE statuses From 6decb9872555ba1c194356ab85fb164ba0f81d28 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Wed, 16 Oct 2024 16:47:30 -0400 Subject: [PATCH 110/113] assert --- internal/tss/router/router_test.go | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/internal/tss/router/router_test.go b/internal/tss/router/router_test.go index cbf5200..3de08ce 100644 --- a/internal/tss/router/router_test.go +++ b/internal/tss/router/router_test.go @@ -33,8 +33,9 @@ func TestRouter(t *testing.T) { Return(). Once() - _ = router.Route(payload) + err := router.Route(payload) + assert.NoError(t, err) rpcCallerChannel.AssertCalled(t, "Send", payload) }) t.Run("status_try_again_later_routes_to_error_jitter_channel", func(t *testing.T) { @@ -46,8 +47,9 @@ func TestRouter(t *testing.T) { Return(). Once() - _ = router.Route(payload) + err := router.Route(payload) + assert.NoError(t, err) errorJitterChannel.AssertCalled(t, "Send", payload) }) @@ -60,8 +62,9 @@ func TestRouter(t *testing.T) { Return(). Once() - _ = router.Route(payload) + err := router.Route(payload) + assert.NoError(t, err) webhookChannel.AssertCalled(t, "Send", payload) }) @@ -74,8 +77,9 @@ func TestRouter(t *testing.T) { Return(). Once() - _ = router.Route(payload) + err := router.Route(payload) + assert.NoError(t, err) webhookChannel.AssertCalled(t, "Send", payload) }) @@ -97,8 +101,9 @@ func TestRouter(t *testing.T) { Return(). Once() - _ = router.Route(payload) + err := router.Route(payload) + assert.NoError(t, err) errorJitterChannel.AssertCalled(t, "Send", payload) } }) @@ -120,8 +125,9 @@ func TestRouter(t *testing.T) { Return(). Once() - _ = router.Route(payload) + err := router.Route(payload) + assert.NoError(t, err) errorNonJitterChannel.AssertCalled(t, "Send", payload) } }) @@ -142,8 +148,9 @@ func TestRouter(t *testing.T) { Return(). Once() - _ = router.Route(payload) + err := router.Route(payload) + assert.NoError(t, err) webhookChannel.AssertCalled(t, "Send", payload) } }) @@ -162,8 +169,8 @@ func TestRouter(t *testing.T) { Once() err := router.Route(payload) - assert.NoError(t, err) + assert.NoError(t, err) webhookChannel.AssertCalled(t, "Send", payload) }) t.Run("nil_channel_does_not_route", func(t *testing.T) { From 693764b7102be2a0d30c5ff840f6c42d10c7a434 Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Wed, 23 Oct 2024 12:16:36 -0400 Subject: [PATCH 111/113] changes based on comments --- ...24-10-14.0-alter_tss_transaction_tries.sql | 12 +++++----- internal/serve/serve.go | 5 +--- internal/tss/services/pool_populator.go | 16 ++++++------- .../tss/services/transaction_manager_test.go | 23 +++++++++++-------- internal/tss/store/store.go | 10 ++++---- 5 files changed, 34 insertions(+), 32 deletions(-) diff --git a/internal/db/migrations/2024-10-14.0-alter_tss_transaction_tries.sql b/internal/db/migrations/2024-10-14.0-alter_tss_transaction_tries.sql index 4828f6e..4dee34b 100644 --- a/internal/db/migrations/2024-10-14.0-alter_tss_transaction_tries.sql +++ b/internal/db/migrations/2024-10-14.0-alter_tss_transaction_tries.sql @@ -1,15 +1,15 @@ -- +migrate Up +ALTER TABLE tss_transaction_submission_tries + RENAME COLUMN status TO code; ALTER TABLE tss_transaction_submission_tries - DROP column status, ADD column status TEXT NOT NULL, - ADD COLUMN code INTEGER NOT NULL, ADD COLUMN result_xdr TEXT NOT NULL; -- +migrate Down +ALTER TABLE tss_transaction_submission_tries + DROP COLUMN status, + DROP COLUMN result_xdr; ALTER TABLE tss_transaction_submission_tries - DROP column status, - DROP COLUMN code, - DROP COLUMN result_xdr, - ADD column status INTEGER NOT NULL; + RENAME COLUMN code TO status; \ No newline at end of file diff --git a/internal/serve/serve.go b/internal/serve/serve.go index 57be014..bcf25b3 100644 --- a/internal/serve/serve.go +++ b/internal/serve/serve.go @@ -289,10 +289,7 @@ func populatePools(poolPopulator tssservices.PoolPopulator) { ctx := context.Background() for range ticker.C { - err := poolPopulator.PopulatePools(ctx) - if err != nil { - log.Ctx(ctx).Error("Populating pools...") - } + poolPopulator.PopulatePools(ctx) } } diff --git a/internal/tss/services/pool_populator.go b/internal/tss/services/pool_populator.go index bb948c3..5a78d44 100644 --- a/internal/tss/services/pool_populator.go +++ b/internal/tss/services/pool_populator.go @@ -6,6 +6,7 @@ import ( "slices" "time" + "github.com/stellar/go/support/log" "github.com/stellar/go/txnbuild" "github.com/stellar/go/xdr" "github.com/stellar/wallet-backend/internal/entities" @@ -16,7 +17,7 @@ import ( ) type PoolPopulator interface { - PopulatePools(ctx context.Context) error + PopulatePools(ctx context.Context) } type poolPopulator struct { @@ -42,32 +43,31 @@ func NewPoolPopulator(router router.Router, store store.Store, rpcService servic }, nil } -func (p *poolPopulator) PopulatePools(ctx context.Context) error { +func (p *poolPopulator) PopulatePools(ctx context.Context) { err := p.routeNewTransactions(ctx) if err != nil { - return fmt.Errorf("error routing new transactions: %w", err) + log.Ctx(ctx).Errorf("error routing new transactions: %v", err) } err = p.routeErrorTransactions(ctx) if err != nil { - return fmt.Errorf("error routing new transactions: %w", err) + log.Ctx(ctx).Errorf("error routing error transactions: %v", err) } err = p.routeFinalTransactions(ctx, tss.RPCTXStatus{RPCStatus: entities.FailedStatus}) if err != nil { - return fmt.Errorf("error routing failed transactions: %w", err) + log.Ctx(ctx).Errorf("error routing failed transactions: %v", err) } err = p.routeFinalTransactions(ctx, tss.RPCTXStatus{RPCStatus: entities.SuccessStatus}) if err != nil { - return fmt.Errorf("error routing successful transactions: %w", err) + log.Ctx(ctx).Errorf("error routing successful transactions: %v", err) } err = p.routeNotSentTransactions(ctx) if err != nil { - return fmt.Errorf("error routing not_sent transactions: %w", err) + log.Ctx(ctx).Errorf("error routing not_sent transactions: %v", err) } - return nil } func (p *poolPopulator) routeNewTransactions(ctx context.Context) error { diff --git a/internal/tss/services/transaction_manager_test.go b/internal/tss/services/transaction_manager_test.go index 7beb2fa..0071200 100644 --- a/internal/tss/services/transaction_manager_test.go +++ b/internal/tss/services/transaction_manager_test.go @@ -48,8 +48,9 @@ func TestBuildAndSubmitTransaction(t *testing.T) { Return(nil, errors.New("signing failed")). Once() - _, err := txManager.BuildAndSubmitTransaction(context.Background(), "channel", payload) + txSendResp, err := txManager.BuildAndSubmitTransaction(context.Background(), "channel", payload) + assert.Equal(t, tss.RPCSendTxResponse{}, txSendResp) assert.Equal(t, "channel: Unable to sign/build transaction: signing failed", err.Error()) tx, _ := store.GetTransaction(context.Background(), payload.TransactionHash) @@ -72,8 +73,10 @@ func TestBuildAndSubmitTransaction(t *testing.T) { Return(sendResp, errors.New("RPC down")). Once() - _, err := txManager.BuildAndSubmitTransaction(context.Background(), "channel", payload) + txSendResp, err := txManager.BuildAndSubmitTransaction(context.Background(), "channel", payload) + assert.Equal(t, entities.ErrorStatus, txSendResp.Status.RPCStatus) + assert.Equal(t, tss.RPCFailCode, txSendResp.Code.OtherCodes) assert.Equal(t, "channel: RPC fail: RPC fail: RPC down", err.Error()) tx, _ := store.GetTransaction(context.Background(), payload.TransactionHash) @@ -103,10 +106,10 @@ func TestBuildAndSubmitTransaction(t *testing.T) { Return(sendResp, nil). Once() - resp, err := txManager.BuildAndSubmitTransaction(context.Background(), "channel", payload) + txSendResp, err := txManager.BuildAndSubmitTransaction(context.Background(), "channel", payload) - assert.Equal(t, entities.PendingStatus, resp.Status.RPCStatus) - assert.Equal(t, tss.EmptyCode, resp.Code.OtherCodes) + assert.Equal(t, entities.PendingStatus, txSendResp.Status.RPCStatus) + assert.Equal(t, tss.EmptyCode, txSendResp.Code.OtherCodes) assert.Empty(t, err) tx, _ := store.GetTransaction(context.Background(), payload.TransactionHash) @@ -136,8 +139,10 @@ func TestBuildAndSubmitTransaction(t *testing.T) { Return(sendResp, nil). Once() - _, err := txManager.BuildAndSubmitTransaction(context.Background(), "channel", payload) + txSendResp, err := txManager.BuildAndSubmitTransaction(context.Background(), "channel", payload) + assert.Equal(t, entities.ErrorStatus, txSendResp.Status.RPCStatus) + assert.Equal(t, tss.UnmarshalBinaryCode, txSendResp.Code.OtherCodes) assert.Equal(t, "channel: RPC fail: parse error result xdr string: unable to parse: unable to unmarshal errorResultXDR: ABCD", err.Error()) tx, _ := store.GetTransaction(context.Background(), payload.TransactionHash) @@ -167,10 +172,10 @@ func TestBuildAndSubmitTransaction(t *testing.T) { Return(sendResp, nil). Once() - resp, err := txManager.BuildAndSubmitTransaction(context.Background(), "channel", payload) + txSendResp, err := txManager.BuildAndSubmitTransaction(context.Background(), "channel", payload) - assert.Equal(t, entities.ErrorStatus, resp.Status.RPCStatus) - assert.Equal(t, xdr.TransactionResultCodeTxTooLate, resp.Code.TxResultCode) + assert.Equal(t, entities.ErrorStatus, txSendResp.Status.RPCStatus) + assert.Equal(t, xdr.TransactionResultCodeTxTooLate, txSendResp.Code.TxResultCode) assert.Empty(t, err) tx, _ := store.GetTransaction(context.Background(), payload.TransactionHash) diff --git a/internal/tss/store/store.go b/internal/tss/store/store.go index 032dbd6..2ef0661 100644 --- a/internal/tss/store/store.go +++ b/internal/tss/store/store.go @@ -99,7 +99,7 @@ func (s *store) UpsertTry(ctx context.Context, txHash string, feeBumpTxHash stri } func (s *store) GetTransaction(ctx context.Context, hash string) (Transaction, error) { - q := `SELECT * from tss_transactions where transaction_hash = $1` + q := `SELECT * FROM tss_transactions WHERE transaction_hash = $1` var transaction Transaction err := s.DB.GetContext(ctx, &transaction, q, hash) if err != nil { @@ -112,7 +112,7 @@ func (s *store) GetTransaction(ctx context.Context, hash string) (Transaction, e } func (s *store) GetTry(ctx context.Context, hash string) (Try, error) { - q := `SELECT * from tss_transaction_submission_tries where try_transaction_hash = $1` + q := `SELECT * FROM tss_transaction_submission_tries WHERE try_transaction_hash = $1` var try Try err := s.DB.GetContext(ctx, &try, q, hash) if err != nil { @@ -125,7 +125,7 @@ func (s *store) GetTry(ctx context.Context, hash string) (Try, error) { } func (s *store) GetTryByXDR(ctx context.Context, xdr string) (Try, error) { - q := `SELECT * from tss_transaction_submission_tries where try_transaction_xdr = $1` + q := `SELECT * FROM tss_transaction_submission_tries WHERE try_transaction_xdr = $1` var try Try err := s.DB.GetContext(ctx, &try, q, xdr) if err != nil { @@ -138,7 +138,7 @@ func (s *store) GetTryByXDR(ctx context.Context, xdr string) (Try, error) { } func (s *store) GetTransactionsWithStatus(ctx context.Context, status tss.RPCTXStatus) ([]Transaction, error) { - q := `SELECT * from tss_transactions where current_status = $1` + q := `SELECT * FROM tss_transactions WHERE current_status = $1` var transactions []Transaction err := s.DB.SelectContext(ctx, &transactions, q, status.Status()) if err != nil { @@ -151,7 +151,7 @@ func (s *store) GetTransactionsWithStatus(ctx context.Context, status tss.RPCTXS } func (s *store) GetLatestTry(ctx context.Context, txHash string) (Try, error) { - q := `SELECT * from tss_transaction_submission_tries where original_transaction_hash = $1 ORDER BY updated_at DESC LIMIT 1` + q := `SELECT * FROM tss_transaction_submission_tries WHERE original_transaction_hash = $1 ORDER BY updated_at DESC LIMIT 1` var try Try err := s.DB.GetContext(ctx, &try, q, txHash) if err != nil { From 7e8f3baaad261580e3565f7bb9486027d56785ce Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Wed, 23 Oct 2024 19:41:04 -0400 Subject: [PATCH 112/113] renaming tss_options --- cmd/serve.go | 28 +++++++------- cmd/utils/tss_options.go | 80 ++++++++++++++++++++-------------------- 2 files changed, 54 insertions(+), 54 deletions(-) diff --git a/cmd/serve.go b/cmd/serve.go index 320e798..2108e26 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -30,23 +30,23 @@ func (c *serveCmd) Command() *cobra.Command { utils.BaseFeeOption(&cfg.BaseFee), utils.HorizonClientURLOption(&cfg.HorizonClientURL), utils.RPCURLOption(&cfg.RPCURL), - utils.RPCCallerServiceChannelBufferSizeOption(&cfg.RPCCallerServiceChannelBufferSize), - utils.RPCCallerServiceMaxWorkersOption(&cfg.RPCCallerServiceChannelMaxWorkers), + utils.RPCCallerChannelBufferSizeOption(&cfg.RPCCallerServiceChannelBufferSize), + utils.RPCCallerChannelMaxWorkersOption(&cfg.RPCCallerServiceChannelMaxWorkers), utils.ChannelAccountEncryptionPassphraseOption(&cfg.EncryptionPassphrase), utils.SentryDSNOption(&sentryDSN), utils.StellarEnvironmentOption(&stellarEnvironment), - utils.ErrorHandlerServiceJitterChannelBufferSizeOption(&cfg.ErrorHandlerServiceJitterChannelBufferSize), - utils.ErrorHandlerServiceJitterChannelMaxWorkersOption(&cfg.ErrorHandlerServiceJitterChannelMaxWorkers), - utils.ErrorHandlerServiceNonJitterChannelBufferSizeOption(&cfg.ErrorHandlerServiceNonJitterChannelBufferSize), - utils.ErrorHandlerServiceNonJitterChannelMaxWorkersOption(&cfg.ErrorHandlerServiceNonJitterChannelMaxWorkers), - utils.ErrorHandlerServiceJitterChannelMinWaitBtwnRetriesMSOption(&cfg.ErrorHandlerServiceJitterChannelMinWaitBtwnRetriesMS), - utils.ErrorHandlerServiceNonJitterChannelWaitBtwnRetriesMSOption(&cfg.ErrorHandlerServiceNonJitterChannelWaitBtwnRetriesMS), - utils.ErrorHandlerServiceJitterChannelMaxRetriesOptions(&cfg.ErrorHandlerServiceJitterChannelMaxRetries), - utils.ErrorHandlerServiceNonJitterChannelMaxRetriesOption(&cfg.ErrorHandlerServiceNonJitterChannelMaxRetries), - utils.WebhookHandlerServiceChannelMaxBufferSizeOption(&cfg.WebhookHandlerServiceChannelMaxBufferSize), - utils.WebhookHandlerServiceChannelMaxWorkersOptions(&cfg.WebhookHandlerServiceChannelMaxWorkers), - utils.WebhookHandlerServiceChannelMaxRetriesOption(&cfg.WebhookHandlerServiceChannelMaxRetries), - utils.WebhookHandlerServiceChannelMinWaitBtwnRetriesMSOption(&cfg.WebhookHandlerServiceChannelMinWaitBtwnRetriesMS), + utils.ErrorHandlerJitterChannelBufferSizeOption(&cfg.ErrorHandlerServiceJitterChannelBufferSize), + utils.ErrorHandlerJitterChannelMaxWorkersOption(&cfg.ErrorHandlerServiceJitterChannelMaxWorkers), + utils.ErrorHandlerNonJitterChannelBufferSizeOption(&cfg.ErrorHandlerServiceNonJitterChannelBufferSize), + utils.ErrorHandlerNonJitterChannelMaxWorkersOption(&cfg.ErrorHandlerServiceNonJitterChannelMaxWorkers), + utils.ErrorHandlerJitterChannelMinWaitBtwnRetriesMSOption(&cfg.ErrorHandlerServiceJitterChannelMinWaitBtwnRetriesMS), + utils.ErrorHandlerNonJitterChannelWaitBtwnRetriesMSOption(&cfg.ErrorHandlerServiceNonJitterChannelWaitBtwnRetriesMS), + utils.ErrorHandlerJitterChannelMaxRetriesOptions(&cfg.ErrorHandlerServiceJitterChannelMaxRetries), + utils.ErrorHandlerNonJitterChannelMaxRetriesOption(&cfg.ErrorHandlerServiceNonJitterChannelMaxRetries), + utils.WebhookHandlerChannelMaxBufferSizeOption(&cfg.WebhookHandlerServiceChannelMaxBufferSize), + utils.WebhookHandlerChannelMaxWorkersOptions(&cfg.WebhookHandlerServiceChannelMaxWorkers), + utils.WebhookHandlerChannelMaxRetriesOption(&cfg.WebhookHandlerServiceChannelMaxRetries), + utils.WebhookHandlerChannelMinWaitBtwnRetriesMSOption(&cfg.WebhookHandlerServiceChannelMinWaitBtwnRetriesMS), { Name: "port", diff --git a/cmd/utils/tss_options.go b/cmd/utils/tss_options.go index 7a3e9b0..6cb729b 100644 --- a/cmd/utils/tss_options.go +++ b/cmd/utils/tss_options.go @@ -6,20 +6,20 @@ import ( "github.com/stellar/go/support/config" ) -func RPCCallerServiceChannelBufferSizeOption(configKey *int) *config.ConfigOption { +func RPCCallerChannelBufferSizeOption(configKey *int) *config.ConfigOption { return &config.ConfigOption{ - Name: "tss-rpc-caller-service-channel-buffer-size", - Usage: "Set the buffer size for TSS RPC Caller Service channel.", + Name: "tss-rpc-caller-channel-buffer-size", + Usage: "Set the buffer size for TSS RPC Caller channel.", OptType: types.Int, ConfigKey: configKey, FlagDefault: 1000, } } -func RPCCallerServiceMaxWorkersOption(configKey *int) *config.ConfigOption { +func RPCCallerChannelMaxWorkersOption(configKey *int) *config.ConfigOption { return &config.ConfigOption{ - Name: "tss-rpc-caller-service-channel-max-workers", - Usage: "Set the maximum number of workers for TSS RPC Caller Service channel.", + Name: "tss-rpc-caller-channel-max-workers", + Usage: "Set the maximum number of workers for TSS RPC Caller channel.", OptType: types.Int, ConfigKey: configKey, FlagDefault: 100, @@ -27,10 +27,10 @@ func RPCCallerServiceMaxWorkersOption(configKey *int) *config.ConfigOption { } -func ErrorHandlerServiceJitterChannelBufferSizeOption(configKey *int) *config.ConfigOption { +func ErrorHandlerJitterChannelBufferSizeOption(configKey *int) *config.ConfigOption { return &config.ConfigOption{ - Name: "error-handler-service-jitter-channel-buffer-size", - Usage: "Set the buffer size of the Error Handler Service Jitter channel.", + Name: "error-handler-jitter-channel-buffer-size", + Usage: "Set the buffer size of the Error Handler Jitter channel.", OptType: types.Int, ConfigKey: configKey, FlagDefault: 100, @@ -38,10 +38,10 @@ func ErrorHandlerServiceJitterChannelBufferSizeOption(configKey *int) *config.Co } } -func ErrorHandlerServiceJitterChannelMaxWorkersOption(configKey *int) *config.ConfigOption { +func ErrorHandlerJitterChannelMaxWorkersOption(configKey *int) *config.ConfigOption { return &config.ConfigOption{ - Name: "error-handler-service-jitter-channel-max-workers", - Usage: "Set the maximum number of workers for the Error Handler Service Jitter channel.", + Name: "error-handler-jitter-channel-max-workers", + Usage: "Set the maximum number of workers for the Error Handler Jitter channel.", OptType: types.Int, ConfigKey: configKey, FlagDefault: 10, @@ -49,10 +49,10 @@ func ErrorHandlerServiceJitterChannelMaxWorkersOption(configKey *int) *config.Co } } -func ErrorHandlerServiceNonJitterChannelBufferSizeOption(configKey *int) *config.ConfigOption { +func ErrorHandlerNonJitterChannelBufferSizeOption(configKey *int) *config.ConfigOption { return &config.ConfigOption{ - Name: "error-handler-service-non-jitter-channel-buffer-size", - Usage: "Set the buffer size of the Error Handler Service Non Jitter channel.", + Name: "error-handler-non-jitter-channel-buffer-size", + Usage: "Set the buffer size of the Error Handler Non Jitter channel.", OptType: types.Int, ConfigKey: configKey, FlagDefault: 100, @@ -61,10 +61,10 @@ func ErrorHandlerServiceNonJitterChannelBufferSizeOption(configKey *int) *config } -func ErrorHandlerServiceNonJitterChannelMaxWorkersOption(configKey *int) *config.ConfigOption { +func ErrorHandlerNonJitterChannelMaxWorkersOption(configKey *int) *config.ConfigOption { return &config.ConfigOption{ - Name: "error-handler-service-non-jitter-channel-max-workers", - Usage: "Set the maximum number of workers for the Error Handler Service Non Jitter channel.", + Name: "error-handler-non-jitter-channel-max-workers", + Usage: "Set the maximum number of workers for the Error Handler Non Jitter channel.", OptType: types.Int, ConfigKey: configKey, FlagDefault: 10, @@ -72,10 +72,10 @@ func ErrorHandlerServiceNonJitterChannelMaxWorkersOption(configKey *int) *config } } -func ErrorHandlerServiceJitterChannelMinWaitBtwnRetriesMSOption(configKey *int) *config.ConfigOption { +func ErrorHandlerJitterChannelMinWaitBtwnRetriesMSOption(configKey *int) *config.ConfigOption { return &config.ConfigOption{ - Name: "error-handler-service-jitter-channel-min-wait-between-retries", - Usage: "Set the minimum amount of time in ms between retries for the Error Handler Service Jitter channel.", + Name: "error-handler-jitter-channel-min-wait-between-retries", + Usage: "Set the minimum amount of time in ms between retries for the Error Handler Jitter channel.", OptType: types.Int, ConfigKey: configKey, FlagDefault: 10, @@ -83,10 +83,10 @@ func ErrorHandlerServiceJitterChannelMinWaitBtwnRetriesMSOption(configKey *int) } } -func ErrorHandlerServiceNonJitterChannelWaitBtwnRetriesMSOption(configKey *int) *config.ConfigOption { +func ErrorHandlerNonJitterChannelWaitBtwnRetriesMSOption(configKey *int) *config.ConfigOption { return &config.ConfigOption{ - Name: "error-handler-service-non-jitter-channel-wait-between-retries", - Usage: "Set the amount of time in ms between retries for the Error Handler Service Non Jitter channel.", + Name: "error-handler-non-jitter-channel-wait-between-retries", + Usage: "Set the amount of time in ms between retries for the Error Handler Non Jitter channel.", OptType: types.Int, ConfigKey: configKey, FlagDefault: 10, @@ -94,10 +94,10 @@ func ErrorHandlerServiceNonJitterChannelWaitBtwnRetriesMSOption(configKey *int) } } -func ErrorHandlerServiceJitterChannelMaxRetriesOptions(configKey *int) *config.ConfigOption { +func ErrorHandlerJitterChannelMaxRetriesOptions(configKey *int) *config.ConfigOption { return &config.ConfigOption{ - Name: "error-handler-service-jitter-channel-max-retries", - Usage: "Set the number of retries for each task in the Error Handler Service Jitter channel.", + Name: "error-handler-jitter-channel-max-retries", + Usage: "Set the number of retries for each task in the Error Handler Jitter channel.", OptType: types.Int, ConfigKey: configKey, FlagDefault: 10, @@ -106,10 +106,10 @@ func ErrorHandlerServiceJitterChannelMaxRetriesOptions(configKey *int) *config.C } -func ErrorHandlerServiceNonJitterChannelMaxRetriesOption(configKey *int) *config.ConfigOption { +func ErrorHandlerNonJitterChannelMaxRetriesOption(configKey *int) *config.ConfigOption { return &config.ConfigOption{ - Name: "error-handler-service-non-jitter-channel-max-retries", - Usage: "Set the number of retries for each task in the Error Handler Service Non Jitter channel.", + Name: "error-handler-non-jitter-channel-max-retries", + Usage: "Set the number of retries for each task in the Error Handler Service Jitter channel.", OptType: types.Int, ConfigKey: configKey, FlagDefault: 10, @@ -117,10 +117,10 @@ func ErrorHandlerServiceNonJitterChannelMaxRetriesOption(configKey *int) *config } } -func WebhookHandlerServiceChannelMaxBufferSizeOption(configKey *int) *config.ConfigOption { +func WebhookHandlerChannelMaxBufferSizeOption(configKey *int) *config.ConfigOption { return &config.ConfigOption{ - Name: "webhook-service-channel-max-buffer-size", - Usage: "Set the buffer size of the webhook serive channel.", + Name: "webhook-channel-max-buffer-size", + Usage: "Set the buffer size of the webhook channel.", OptType: types.Int, ConfigKey: configKey, FlagDefault: 100, @@ -128,10 +128,10 @@ func WebhookHandlerServiceChannelMaxBufferSizeOption(configKey *int) *config.Con } } -func WebhookHandlerServiceChannelMaxWorkersOptions(configKey *int) *config.ConfigOption { +func WebhookHandlerChannelMaxWorkersOptions(configKey *int) *config.ConfigOption { return &config.ConfigOption{ - Name: "webhook-service-channel-max-workers", - Usage: "Set the max number of workers for the webhook serive channel.", + Name: "webhook-channel-max-workers", + Usage: "Set the max number of workers for the webhook channel.", OptType: types.Int, ConfigKey: configKey, FlagDefault: 10, @@ -139,9 +139,9 @@ func WebhookHandlerServiceChannelMaxWorkersOptions(configKey *int) *config.Confi } } -func WebhookHandlerServiceChannelMaxRetriesOption(configKey *int) *config.ConfigOption { +func WebhookHandlerChannelMaxRetriesOption(configKey *int) *config.ConfigOption { return &config.ConfigOption{ - Name: "webhook-service-channel-max-retries", + Name: "webhook-channel-max-retries", Usage: "Set the max number of times to ping a webhook before quitting.", OptType: types.Int, ConfigKey: configKey, @@ -150,9 +150,9 @@ func WebhookHandlerServiceChannelMaxRetriesOption(configKey *int) *config.Config } } -func WebhookHandlerServiceChannelMinWaitBtwnRetriesMSOption(configKey *int) *config.ConfigOption { +func WebhookHandlerChannelMinWaitBtwnRetriesMSOption(configKey *int) *config.ConfigOption { return &config.ConfigOption{ - Name: "webhook-service-channel-min-wait-between-retries", + Name: "webhook-channel-min-wait-between-retries", Usage: "The minumum amout of time to wait before resending the payload to the webhook url", OptType: types.Int, ConfigKey: configKey, From c67560ad4fcdc5ac417ec534c078ff5177df455e Mon Sep 17 00:00:00 2001 From: gouthamp-stellar Date: Wed, 23 Oct 2024 19:41:36 -0400 Subject: [PATCH 113/113] oops --- cmd/ingest.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cmd/ingest.go b/cmd/ingest.go index 32dab4d..581dcf5 100644 --- a/cmd/ingest.go +++ b/cmd/ingest.go @@ -30,10 +30,10 @@ func (c *ingestCmd) Command() *cobra.Command { utils.RPCURLOption(&cfg.RPCURL), utils.StartLedgerOption(&cfg.StartLedger), utils.EndLedgerOption(&cfg.EndLedger), - utils.WebhookHandlerServiceChannelMaxBufferSizeOption(&cfg.WebhookChannelMaxBufferSize), - utils.WebhookHandlerServiceChannelMaxWorkersOptions(&cfg.WebhookChannelMaxWorkers), - utils.WebhookHandlerServiceChannelMaxRetriesOption(&cfg.WebhookChannelMaxRetries), - utils.WebhookHandlerServiceChannelMinWaitBtwnRetriesMSOption(&cfg.WebhookChannelWaitBtwnTriesMS), + utils.WebhookHandlerChannelMaxBufferSizeOption(&cfg.WebhookChannelMaxBufferSize), + utils.WebhookHandlerChannelMaxWorkersOptions(&cfg.WebhookChannelMaxWorkers), + utils.WebhookHandlerChannelMaxRetriesOption(&cfg.WebhookChannelMaxRetries), + utils.WebhookHandlerChannelMinWaitBtwnRetriesMSOption(&cfg.WebhookChannelWaitBtwnTriesMS), { Name: "ledger-cursor-name", Usage: "Name of last synced ledger cursor, used to keep track of the last ledger ingested by the service. When starting up, ingestion will resume from the ledger number stored in this record. It should be an unique name per container as different containers would overwrite the cursor value of its peers when using the same cursor name.",