diff --git a/CHANGELOG.md b/CHANGELOG.md index 77c0e4a3..b4872cbc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +#### xrpl + +- Added `LedgerEntry` query. + +## [v0.1.3] + ### Added - Added `APIVersion` field to the `Client` struct. diff --git a/xrpl/queries/ledger/ledger_entry.go b/xrpl/queries/ledger/ledger_entry.go new file mode 100644 index 00000000..a065c808 --- /dev/null +++ b/xrpl/queries/ledger/ledger_entry.go @@ -0,0 +1,65 @@ +package ledger + +import ( + "github.com/Peersyst/xrpl-go/xrpl/queries/common" + "github.com/Peersyst/xrpl-go/xrpl/queries/ledger/types" + "github.com/Peersyst/xrpl-go/xrpl/queries/version" +) + +// ############################################################################ +// Request +// ############################################################################ + +// The `ledger_entry` method returns a single ledger object from the XRP Ledger +// in its raw format. Expects a response in the form of a EntryResponse. +type EntryRequest struct { + common.BaseRequest + MPTIssuance bool `json:"mpt_issuance,omitempty"` + MPToken interface{} `json:"mptoken,omitempty"` + AMM types.EntryAssetPair `json:"amm,omitempty"` + IncludeDeleted bool `json:"include_deleted,omitempty"` + Binary bool `json:"binary,omitempty"` + Index string `json:"index,omitempty"` + AccountRoot string `json:"account_root,omitempty"` + Check string `json:"check,omitempty"` + Credential interface{} `json:"credential,omitempty"` + DepositPreauth interface{} `json:"deposit_preauth,omitempty"` + Did string `json:"did,omitempty"` + Directory interface{} `json:"directory,omitempty"` + Escrow interface{} `json:"escrow,omitempty"` + Offer interface{} `json:"offer,omitempty"` + PaymentChannel string `json:"payment_channel,omitempty"` + RippleState types.EntryRippleState `json:"ripple_state,omitempty"` + Ticket interface{} `json:"ticket,omitempty"` + NFTPage string `json:"nft_page,omitempty"` + BridgeAccount string `json:"bridge_account,omitempty"` + Bridge types.EntryXChainBridge `json:"bridge,omitempty"` + XChainOwnedClaimID interface{} `json:"xchain_owned_claim_id,omitempty"` + XChainOwnedCreateAccountClaimID interface{} `json:"xchain_owned_create_account_claim_id,omitempty"` +} + +func (e *EntryRequest) Method() string { + return "ledger_entry" +} + +func (e *EntryRequest) APIVersion() int { + return version.RippledAPIV2 +} + +func (e *EntryRequest) Validate() error { + return nil +} + +// ############################################################################ +// Response +// ############################################################################ + +// The expected response from the ledger_entry method. +type EntryResponse struct { + Index string `json:"index"` + LedgerCurrentIndex common.LedgerIndex `json:"ledger_current_index"` + Node interface{} `json:"node,omitempty"` + NodeBinary string `json:"node_binary,omitempty"` + Validated bool `json:"validated,omitempty"` + DeletedLedgerIndex common.LedgerIndex `json:"deleted_ledger_index,omitempty"` +} diff --git a/xrpl/queries/ledger/ledger_entry_test.go b/xrpl/queries/ledger/ledger_entry_test.go new file mode 100644 index 00000000..868e9199 --- /dev/null +++ b/xrpl/queries/ledger/ledger_entry_test.go @@ -0,0 +1,78 @@ +package ledger + +import ( + "testing" + + "github.com/Peersyst/xrpl-go/xrpl/ledger-entry-types" + types "github.com/Peersyst/xrpl-go/xrpl/queries/ledger/types" + "github.com/Peersyst/xrpl-go/xrpl/testutil" +) + +func TestLedgerEntryRequest(t *testing.T) { + s := EntryRequest{ + MPTIssuance: true, + Binary: true, + Index: "some_index", + AccountRoot: "some_account", + Check: "some_check", + Did: "some_did", + PaymentChannel: "some_channel", + NFTPage: "some_nft_page", + BridgeAccount: "some_bridge_account", + Bridge: types.EntryXChainBridge{ + LockingChainDoor: "door", + LockingChainIssue: "issue", + IssuingChainDoor: "door2", + IssuingChainIssue: "issue2", + }, + IncludeDeleted: true, + AMM: types.EntryAssetPair{ + Asset: ledger.Asset{ + Currency: "XRP", + }, + Asset2: ledger.Asset{ + Currency: "USD", + }, + }, + RippleState: types.EntryRippleState{ + Accounts: []string{"acc1", "acc2"}, + Currency: "XRP", + }, + } + j := `{ + "mpt_issuance": true, + "amm": { + "asset": { + "currency": "XRP" + }, + "asset2": { + "currency": "USD" + } + }, + "include_deleted": true, + "binary": true, + "index": "some_index", + "account_root": "some_account", + "check": "some_check", + "did": "some_did", + "payment_channel": "some_channel", + "ripple_state": { + "accounts": [ + "acc1", + "acc2" + ], + "currency": "XRP" + }, + "nft_page": "some_nft_page", + "bridge_account": "some_bridge_account", + "bridge": { + "locking_chain_door": "door", + "locking_chain_issue": "issue", + "issuing_chain_door": "door2", + "issuing_chain_issue": "issue2" + } +}` + if err := testutil.Serialize(t, s, j); err != nil { + t.Error(err) + } +} diff --git a/xrpl/queries/ledger/types/entry.go b/xrpl/queries/ledger/types/entry.go new file mode 100644 index 00000000..d96344c0 --- /dev/null +++ b/xrpl/queries/ledger/types/entry.go @@ -0,0 +1,69 @@ +package types + +import ( + "github.com/Peersyst/xrpl-go/xrpl/ledger-entry-types" +) + +type EntryAssetPair struct { + Asset ledger.Asset `json:"asset"` + Asset2 ledger.Asset `json:"asset2"` +} + +type EntryMPToken struct { + MPTIssuanceID string `json:"mp_issuance_id"` + Account string `json:"account"` +} + +type EntryCredential struct { + Subject string `json:"subject"` + Issuer string `json:"issuer"` + CredentialType string `json:"credential_type"` +} + +type EntryDepositPreauth struct { + Owner string `json:"owner"` + Authorized string `json:"authorized"` +} + +type EntryDirectory struct { + SubIndex uint32 `json:"sub_index,omitempty"` + DirRoot string `json:"dir_root,omitempty"` + Owner string `json:"owner,omitempty"` +} + +type EntryEscrow struct { + Owner string `json:"owner"` + Seq string `json:"seq"` +} + +type EntryOffer struct { + Account string `json:"account"` + Seq string `json:"seq"` +} + +type EntryRippleState struct { + Accounts []string `json:"accounts"` + Currency string `json:"currency"` +} + +type EntryTicket struct { + Owner string `json:"owner"` + TicketSequence string `json:"ticket_sequence"` +} + +type EntryXChainBridge struct { + LockingChainDoor string `json:"locking_chain_door"` + LockingChainIssue string `json:"locking_chain_issue"` + IssuingChainDoor string `json:"issuing_chain_door"` + IssuingChainIssue string `json:"issuing_chain_issue"` +} + +type EntryXChainOwnedClaimID struct { + EntryXChainBridge + XChainOwnedClaimID interface{} `json:"xchain_owned_claim_id"` +} + +type EntryXChainOwnedCreateAccountClaimID struct { + EntryXChainBridge + XChainOwnedCreateAccountClaimID interface{} `json:"xchain_owned_create_account_claim_id"` +} diff --git a/xrpl/rpc/queries.go b/xrpl/rpc/queries.go index 20162b9e..417f45b4 100644 --- a/xrpl/rpc/queries.go +++ b/xrpl/rpc/queries.go @@ -196,6 +196,23 @@ func (c *Client) GetLedgerIndex() (common.LedgerIndex, error) { return lr.LedgerIndex, err } +// GetLedgerEntry retrieves information about a specific ledger entry. +// It takes a LedgerEntryRequest as input and returns a LedgerEntryResponse, +// along with any error encountered. +func (c *Client) GetLedgerEntry(req *ledger.EntryRequest) (*ledger.EntryResponse, error) { + res, err := c.Request(req) + if err != nil { + return nil, err + } + + var lr ledger.EntryResponse + err = res.GetResult(&lr) + if err != nil { + return nil, err + } + return &lr, nil +} + // GetClosedLedger retrieves information about the last closed ledger. // It returns a ClosedResponse containing the ledger information and any error encountered. func (c *Client) GetClosedLedger() (*ledger.ClosedResponse, error) { diff --git a/xrpl/rpc/queries_test.go b/xrpl/rpc/queries_test.go index b000a6ab..c2142be7 100644 --- a/xrpl/rpc/queries_test.go +++ b/xrpl/rpc/queries_test.go @@ -996,6 +996,77 @@ func TestClient_GetLedgerIndex(t *testing.T) { } } +func TestClient_GetLedgerEntry(t *testing.T) { + tests := []struct { + name string + mockResponse string + mockStatus int + request *ledgerqueries.EntryRequest + expected *ledgerqueries.EntryResponse + expectedError string + }{ + { + name: "successful response", + mockResponse: `{ + "result": { + "index": "13F1A95D7AAB7108D5CE7EEAF504B2894B8C674E6D68499076441C4837282BF8", + "ledger_current_index": 61809073, + "node_binary": "test", + "validated": true, + "deleted_ledger_index": 0 + } + }`, + mockStatus: 200, + request: &ledgerqueries.EntryRequest{ + Index: "13F1A95D7AAB7108D5CE7EEAF504B2894B8C674E6D68499076441C4837282BF8", + }, + expected: &ledgerqueries.EntryResponse{ + Index: "13F1A95D7AAB7108D5CE7EEAF504B2894B8C674E6D68499076441C4837282BF8", + LedgerCurrentIndex: 61809073, + NodeBinary: "test", + Validated: true, + DeletedLedgerIndex: 0, + }, + }, + { + name: "error response", + mockResponse: `{ + "result": { + "error": "entryNotFound", + "status": "error" + } + }`, + mockStatus: 200, + request: &ledgerqueries.EntryRequest{}, + expectedError: "entryNotFound", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + mc := testutil.JSONRPCMockClient{} + mc.DoFunc = testutil.MockResponse(tt.mockResponse, tt.mockStatus, &mc) + + cfg, err := NewClientConfig("http://testnode/", WithHTTPClient(&mc)) + require.NoError(t, err) + + client := NewClient(cfg) + + resp, err := client.GetLedgerEntry(tt.request) + + if tt.expectedError != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tt.expectedError) + return + } + + require.NoError(t, err) + require.Equal(t, tt.expected, resp) + }) + } +} + + func TestClient_GetClosedLedger(t *testing.T) { tests := []struct { name string diff --git a/xrpl/websocket/queries.go b/xrpl/websocket/queries.go index bf29f22b..02e0bdac 100644 --- a/xrpl/websocket/queries.go +++ b/xrpl/websocket/queries.go @@ -200,6 +200,22 @@ func (c *Client) GetLedgerIndex() (common.LedgerIndex, error) { return lr.LedgerIndex, err } +// GetLedgerEntry retrieves information about a specific ledger entry. +// It takes a LedgerEntryRequest as input and returns a LedgerEntryResponse, +// along with any error encountered. +func (c *Client) GetLedgerEntry(req *ledger.EntryRequest) (*ledger.EntryResponse, error) { + res, err := c.Request(req) + if err != nil { + return nil, err + } + var lr ledger.EntryResponse + err = res.GetResult(&lr) + if err != nil { + return nil, err + } + return &lr, nil +} + // GetClosedLedger retrieves information about the last closed ledger. // It returns a ClosedResponse containing the ledger information and any error encountered. func (c *Client) GetClosedLedger() (*ledger.ClosedResponse, error) { diff --git a/xrpl/websocket/queries_test.go b/xrpl/websocket/queries_test.go index 2b30ff09..505e6007 100644 --- a/xrpl/websocket/queries_test.go +++ b/xrpl/websocket/queries_test.go @@ -658,6 +658,97 @@ func TestClient_GetLedgerIndex(t *testing.T) { } } +func TestClient_GetLedgerEntry(t *testing.T) { + tests := []struct { + name string + serverMessages []map[string]any + request *ledgerqueries.EntryRequest + expected *ledgerqueries.EntryResponse + expectedErr error + }{ + { + name: "Valid ledger entry", + serverMessages: []map[string]any{ + { + "id": 1, + "result": map[string]any{ + "index": "13F1A95D7AAB7108D5CE7EEAF504B2894B8C674E6D68499076441C4837282BF8", + "ledger_current_index": uint32(61809073), + "node_binary": "test", + "validated": true, + "deleted_ledger_index": uint32(0), + }, + }, + }, + request: &ledgerqueries.EntryRequest{ + Index: "13F1A95D7AAB7108D5CE7EEAF504B2894B8C674E6D68499076441C4837282BF8", + }, + expected: &ledgerqueries.EntryResponse{ + Index: "13F1A95D7AAB7108D5CE7EEAF504B2894B8C674E6D68499076441C4837282BF8", + LedgerCurrentIndex: 61809073, + NodeBinary: "test", + Validated: true, + DeletedLedgerIndex: 0, + }, + expectedErr: nil, + }, + { + name: "Error response", + serverMessages: []map[string]any{{"error": "Entry not found"}}, + request: &ledgerqueries.EntryRequest{ + Index: "13F1A95D7AAB7108D5CE7EEAF504B2894B8C674E6D68499076441C4837282BF8", + }, + expected: nil, + expectedErr: errors.New("incorrect id"), + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ws := &testutil.MockWebSocketServer{Msgs: tt.serverMessages} + s := ws.TestWebSocketServer(func(c *websocket.Conn) { + for _, m := range tt.serverMessages { + err := c.WriteJSON(m) + if err != nil { + t.Errorf("error writing message: %v", err) + } + } + }) + defer s.Close() + + url, _ := testutil.ConvertHTTPToWS(s.URL) + cl := &Client{ + cfg: ClientConfig{ + host: url, + }, + } + + if err := cl.Connect(); err != nil { + t.Errorf("Error connecting to server: %v", err) + } + + result, err := cl.GetLedgerEntry(tt.request) + + if tt.expectedErr != nil { + if err == nil || err.Error() != tt.expectedErr.Error() { + t.Errorf("Expected error %v, but got %v", tt.expectedErr, err) + } + } else { + if err != nil { + t.Errorf("Unexpected error: %v", err) + } + } + + if !reflect.DeepEqual(tt.expected, result) { + t.Errorf("Expected %+v, but got %+v", tt.expected, result) + } + + cl.Disconnect() + }) + } +} + + func TestClient_GetAccountNFTs(t *testing.T) { tests := []struct { name string