-
Notifications
You must be signed in to change notification settings - Fork 0
wallet: DEX order RPC methods (create/conclude/fill/freeze/list) #2
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+1,065
−0
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,253 @@ | ||
| // Copyright (c) 2026 Mintlayer Institutional FZCO | ||
| // Contact: hello@mintlayer.org | ||
| // | ||
| // Use of this source code is governed by an MIT license | ||
| // that can be found in the LICENSE file. | ||
|
|
||
| package wallet | ||
|
|
||
| import ( | ||
| "context" | ||
| "encoding/json" | ||
| "fmt" | ||
| ) | ||
|
|
||
| // OutputValue is one side of an order: native coins or a token amount. | ||
| type OutputValue struct { | ||
| // Coin selects native ML coins when true, a token amount when false. | ||
| Coin bool | ||
| // TokenID is the bech32 token id; required when Coin is false. | ||
| TokenID string | ||
| // Amount carries the atom amount (and decimal, when the daemon sent one). | ||
| Amount Amount | ||
| } | ||
|
|
||
| func (v OutputValue) MarshalJSON() ([]byte, error) { | ||
| if v.Amount.Atoms == "" && v.Amount.Decimal == "" { | ||
| return nil, fmt.Errorf("wallet: output value requires an amount") | ||
| } | ||
| if v.Coin { | ||
| return json.Marshal(struct { | ||
| Type string `json:"type"` | ||
| Content struct { | ||
| Amount Amount `json:"amount"` | ||
| } `json:"content"` | ||
| }{Type: "Coin", Content: struct { | ||
| Amount Amount `json:"amount"` | ||
| }{Amount: v.Amount}}) | ||
| } | ||
| if v.TokenID == "" { | ||
| return nil, fmt.Errorf("wallet: token OutputValue requires TokenID") | ||
| } | ||
| return json.Marshal(struct { | ||
| Type string `json:"type"` | ||
| Content struct { | ||
| ID string `json:"id"` | ||
| Amount Amount `json:"amount"` | ||
| } `json:"content"` | ||
| }{Type: "Token", Content: struct { | ||
| ID string `json:"id"` | ||
| Amount Amount `json:"amount"` | ||
| }{ID: v.TokenID, Amount: v.Amount}}) | ||
| } | ||
|
|
||
| func (v *OutputValue) UnmarshalJSON(b []byte) error { | ||
| var raw struct { | ||
| Type string `json:"type"` | ||
| Content struct { | ||
| ID string `json:"id"` | ||
| Amount *Amount `json:"amount"` | ||
| } `json:"content"` | ||
| } | ||
| if err := json.Unmarshal(b, &raw); err != nil { | ||
| return err | ||
| } | ||
| switch raw.Type { | ||
| case "Coin": | ||
| v.Coin, v.TokenID = true, "" | ||
| case "Token": | ||
| v.Coin, v.TokenID = false, raw.Content.ID | ||
| default: | ||
| return fmt.Errorf("wallet: unknown output value type %q", raw.Type) | ||
| } | ||
| if raw.Content.Amount == nil || (raw.Content.Amount.Atoms == "" && raw.Content.Amount.Decimal == "") { | ||
| return fmt.Errorf("wallet: output value %q requires an amount", raw.Type) | ||
| } | ||
| v.Amount = *raw.Content.Amount | ||
| if !v.Coin && v.TokenID == "" { | ||
| return fmt.Errorf("wallet: token OutputValue requires TokenID") | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| // CurrencyFilter restricts ListAllActiveOrders to one currency side. | ||
| type CurrencyFilter struct { | ||
| Type string | ||
| Content string | ||
| } | ||
|
|
||
| // CoinFilter matches the native coin. | ||
| func CoinFilter() *CurrencyFilter { return &CurrencyFilter{Type: "Coin"} } | ||
|
|
||
| // TokenFilter matches a token by its bech32 id. | ||
| func TokenFilter(tokenID string) (*CurrencyFilter, error) { | ||
| if tokenID == "" { | ||
| return nil, fmt.Errorf("wallet: TokenFilter requires a token id (use CoinFilter for the native coin)") | ||
| } | ||
| return &CurrencyFilter{Type: "Token", Content: tokenID}, nil | ||
| } | ||
|
|
||
| func (f *CurrencyFilter) MarshalJSON() ([]byte, error) { | ||
| // Coin filters carry no content on the wire (daemon-verified); | ||
| // token filters encode the bech32 id as content. | ||
| if f.Type == "Coin" { | ||
| return json.Marshal(struct { | ||
| Type string `json:"type"` | ||
| }{Type: f.Type}) | ||
| } | ||
| return json.Marshal(struct { | ||
| Type string `json:"type"` | ||
| Content string `json:"content"` | ||
| }{Type: f.Type, Content: f.Content}) | ||
| } | ||
|
|
||
| // OrderState is the live balance snapshot of an own order. | ||
| type OrderState struct { | ||
| AskBalance Amount `json:"ask_balance"` | ||
| GiveBalance Amount `json:"give_balance"` | ||
| Frozen bool `json:"is_frozen"` | ||
| Creation Timestamp `json:"creation_timestamp"` | ||
| } | ||
|
|
||
| // OwnOrder is an order whose conclude key is owned by the account. | ||
| type OwnOrder struct { | ||
| OrderID string `json:"order_id"` | ||
| InitiallyAsked OutputValue `json:"initially_asked"` | ||
| InitiallyGiven OutputValue `json:"initially_given"` | ||
| Existing *OrderState `json:"existing_order_data"` | ||
| MarkedFrozen bool `json:"is_marked_as_frozen_in_wallet"` | ||
| MarkedConcluded bool `json:"is_marked_as_concluded_in_wallet"` | ||
| } | ||
|
|
||
| // ActiveOrder is one order from the account-wide listing. | ||
| type ActiveOrder struct { | ||
| OrderID string `json:"order_id"` | ||
| InitiallyAsked OutputValue `json:"initially_asked"` | ||
| InitiallyGiven OutputValue `json:"initially_given"` | ||
| AskBalance Amount `json:"ask_balance"` | ||
| GiveBalance Amount `json:"give_balance"` | ||
| IsOwn bool `json:"is_own"` | ||
| } | ||
|
|
||
| // OrderCreated is the result of CreateOrder. | ||
| type OrderCreated struct { | ||
| OrderID string `json:"order_id"` | ||
| TxID string `json:"tx_id"` | ||
| Broadcasted bool `json:"broadcasted"` | ||
| } | ||
|
|
||
| // CreateOrderParams configures CreateOrder. | ||
| type CreateOrderParams struct { | ||
| Account uint32 `json:"account"` | ||
| Ask OutputValue `json:"ask"` | ||
| Give OutputValue `json:"give"` | ||
| ConcludeAddress string `json:"conclude_address"` | ||
| Options TxOptions `json:"options"` | ||
| } | ||
|
|
||
| // ConcludeOrderParams configures ConcludeOrder. An empty OutputAddress lets | ||
| // the daemon derive a fresh receive address for the remaining funds. | ||
| type ConcludeOrderParams struct { | ||
| Account uint32 `json:"account"` | ||
| OrderID string `json:"order_id"` | ||
| OutputAddress *string `json:"output_address"` | ||
| Options TxOptions `json:"options"` | ||
| } | ||
|
|
||
| // FillOrderParams configures FillOrder. FillAmount is denominated in the | ||
| // order's ask currency. An empty OutputAddress lets the daemon derive one. | ||
| type FillOrderParams struct { | ||
| Account uint32 `json:"account"` | ||
| OrderID string `json:"order_id"` | ||
| FillAmount Amount `json:"fill_amount_in_ask_currency"` | ||
| OutputAddress *string `json:"output_address"` | ||
| Options TxOptions `json:"options"` | ||
| } | ||
|
|
||
| // FreezeOrderParams configures FreezeOrder. | ||
| type FreezeOrderParams struct { | ||
| Account uint32 `json:"account"` | ||
| OrderID string `json:"order_id"` | ||
| Options TxOptions `json:"options"` | ||
| } | ||
|
|
||
| // ListOrdersParams configures ListAllActiveOrders; nil filters match any | ||
| // currency on that side. | ||
| type ListOrdersParams struct { | ||
| Account uint32 `json:"account"` | ||
| AskCurrency *CurrencyFilter `json:"ask_currency"` | ||
| GiveCurrency *CurrencyFilter `json:"give_currency"` | ||
| } | ||
|
|
||
| // CreateOrder places a DEX order: give `Give`, ask for `Ask`. The order is | ||
| // owned by the conclude key derived from ConcludeAddress. | ||
| func (c *Client) CreateOrder(ctx context.Context, params CreateOrderParams) (*OrderCreated, error) { | ||
| var result OrderCreated | ||
| if err := c.call(ctx, "order_create", params, &result); err != nil { | ||
| return nil, err | ||
| } | ||
| return &result, nil | ||
| } | ||
|
|
||
| // ConcludeOrder closes an order, transferring its remaining funds to the | ||
| // OutputAddress (or a fresh derived address when nil). | ||
| func (c *Client) ConcludeOrder(ctx context.Context, params ConcludeOrderParams) (*SendResult, error) { | ||
| var result SendResult | ||
| if err := c.call(ctx, "order_conclude", params, &result); err != nil { | ||
| return nil, err | ||
| } | ||
| return &result, nil | ||
| } | ||
|
|
||
| // FillOrder fills an existing order partially (or fully) by FillAmount in | ||
| // the order's ask currency; proceeds go to the OutputAddress. | ||
| func (c *Client) FillOrder(ctx context.Context, params FillOrderParams) (*SendResult, error) { | ||
| var result SendResult | ||
| if err := c.call(ctx, "order_fill", params, &result); err != nil { | ||
| return nil, err | ||
| } | ||
| return &result, nil | ||
| } | ||
|
|
||
| // FreezeOrder prevents further fills of an order. Frozen orders can only be | ||
| // concluded afterwards. | ||
| func (c *Client) FreezeOrder(ctx context.Context, params FreezeOrderParams) (*SendResult, error) { | ||
| var result SendResult | ||
| if err := c.call(ctx, "order_freeze", params, &result); err != nil { | ||
| return nil, err | ||
| } | ||
| return &result, nil | ||
| } | ||
|
|
||
| // ListOwnOrders returns orders whose conclude key is owned by the account, | ||
| // including their live balances and wallet-side flags. | ||
| func (c *Client) ListOwnOrders(ctx context.Context, account uint32) ([]OwnOrder, error) { | ||
| params := struct { | ||
| Account uint32 `json:"account"` | ||
| }{Account: account} | ||
| var result []OwnOrder | ||
| if err := c.call(ctx, "order_list_own", params, &result); err != nil { | ||
| return nil, err | ||
| } | ||
| return result, nil | ||
| } | ||
|
|
||
| // ListAllActiveOrders returns all active orders matching the currency pair. | ||
| // A nil filter matches any currency on that side. | ||
| func (c *Client) ListAllActiveOrders(ctx context.Context, params ListOrdersParams) ([]ActiveOrder, error) { | ||
| var result []ActiveOrder | ||
| if err := c.call(ctx, "order_list_all_active", params, &result); err != nil { | ||
| return nil, err | ||
| } | ||
| return result, nil | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
MarshalJSON silently drops Content for any Coin-typed filter, including a zero-value-free misuse like &CurrencyFilter{Type: "Coin", Content: tokenID} or a mutated TokenFilter result, so ListAllActiveOrders would silently match the native coin instead of the intended token. Also, unlike the TokenFilter constructor, no validation ensures a non-Coin Type carries non-empty Content (e.g. &CurrencyFilter{Type: "Token"} marshals as a token filter with empty content). Prefer failing on these inconsistent states instead of silently encoding a possibly different currency.
Suggestion: