diff --git a/CHANGELOG.md b/CHANGELOG.md index 0638894e9..f02fcd019 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,12 @@ This changelog is a work in progress and may contain notes for versions which have not actually been released. Check the [Releases](https://github.com/0xProject/0x-mesh/releases) page to see full release notes and more information about the latest released versions. -## v4.1.0-beta +## v5.0.0-beta + +### Breaking changes 🛠 + +- Removes the `txHashes` key in the `OrderEvent`s emitted from the `orders` JSON-RPC subscription and replaced it with `contractEvents`, an array of decoded order-relevant contract events. Parsing these events allows callers to find every discrete order fill/cancel event. ([#420](https://github.com/0xProject/0x-mesh/pull/420)) +- Renames the `Kind` key in `OrderEvent` to `EndState` to better elucidate that it represents the aggregate change to the orders state since it was last re-validated. As an end state, it does not capture any possible intermediate states the order might have been in since the last re-validation. Intermediate states can be inferred from the `contractEvents` included ([#420](https://github.com/0xProject/0x-mesh/pull/420)) ### Features ✅ diff --git a/browser/ts/index.ts b/browser/ts/index.ts index 701f1cd43..facd19faf 100644 --- a/browser/ts/index.ts +++ b/browser/ts/index.ts @@ -125,13 +125,185 @@ interface WrapperSignedOrder { signature: string; } +export interface ERC20TransferEvent { + from: string; + to: string; + value: BigNumber; +} + +interface WrapperERC20TransferEvent { + from: string; + to: string; + value: string; +} + +export interface ERC20ApprovalEvent { + owner: string; + spender: string; + value: BigNumber; +} + +interface WrapperERC20ApprovalEvent { + owner: string; + spender: string; + value: string; +} + +export interface ERC721TransferEvent { + from: string; + to: string; + tokenId: BigNumber; +} + +interface WrapperERC721TransferEvent { + from: string; + to: string; + tokenId: string; +} + +export interface ERC721ApprovalEvent { + owner: string; + approved: string; + tokenId: BigNumber; +} + +interface WrapperERC721ApprovalEvent { + owner: string; + approved: string; + tokenId: string; +} + +export interface ERC721ApprovalForAllEvent { + owner: string; + operator: string; + approved: boolean; +} + +export interface ExchangeFillEvent { + makerAddress: string; + takerAddress: string; + senderAddress: string; + feeRecipientAddress: string; + makerAssetFilledAmount: BigNumber; + takerAssetFilledAmount: BigNumber; + makerFeePaid: BigNumber; + takerFeePaid: BigNumber; + orderHash: string; + makerAssetData: string; + takerAssetData: string; +} + +interface WrapperExchangeFillEvent { + makerAddress: string; + takerAddress: string; + senderAddress: string; + feeRecipientAddress: string; + makerAssetFilledAmount: string; + takerAssetFilledAmount: string; + makerFeePaid: string; + takerFeePaid: string; + orderHash: string; + makerAssetData: string; + takerAssetData: string; +} + +export interface ExchangeCancelEvent { + makerAddress: string; + senderAddress: string; + feeRecipientAddress: string; + orderHash: string; + makerAssetData: string; + takerAssetData: string; +} + +export interface ExchangeCancelUpToEvent { + makerAddress: string; + senderAddress: string; + orderEpoch: BigNumber; +} + +interface WrapperExchangeCancelUpToEvent { + makerAddress: string; + senderAddress: string; + orderEpoch: string; +} + +export interface WethWithdrawalEvent { + owner: string; + value: BigNumber; +} + +interface WrapperWethWithdrawalEvent { + owner: string; + value: string; +} + +export interface WethDepositEvent { + owner: string; + value: BigNumber; +} + +interface WrapperWethDepositEvent { + owner: string; + value: string; +} + +enum ContractEventKind { + ERC20TransferEvent = 'ERC20TransferEvent', + ERC20ApprovalEvent = 'ERC20ApprovalEvent', + ERC721TransferEvent = 'ERC721TransferEvent', + ERC721ApprovalEvent = 'ERC721ApprovalEvent', + ExchangeFillEvent = 'ExchangeFillEvent', + ExchangeCancelEvent = 'ExchangeCancelEvent', + ExchangeCancelUpToEvent = 'ExchangeCancelUpToEvent', + WethDepositEvent = 'WethDepositEvent', + WethWithdrawalEvent = 'WethWithdrawalEvent', +} + +type WrapperContractEventParameters = WrapperERC20TransferEvent | WrapperERC20ApprovalEvent | WrapperERC721TransferEvent | WrapperERC721ApprovalEvent | WrapperExchangeFillEvent | WrapperExchangeCancelUpToEvent | WrapperWethWithdrawalEvent | WrapperWethDepositEvent | ERC721ApprovalForAllEvent | ExchangeCancelEvent; + +type ContractEventParameters = ERC20TransferEvent | ERC20ApprovalEvent | ERC721TransferEvent | ERC721ApprovalEvent | ExchangeFillEvent | ExchangeCancelUpToEvent | WethWithdrawalEvent | WethDepositEvent | ERC721ApprovalForAllEvent | ExchangeCancelEvent; + +export interface ContractEvent { + blockHash: string; + txHash: string; + txIndex: number; + logIndex: number; + isRemoved: string; + address: string; + kind: ContractEventKind; + parameters: ContractEventParameters; +} + // The type for order events exposed by MeshWrapper. +interface WrapperContractEvent { + blockHash: string; + txHash: string; + txIndex: number; + logIndex: number; + isRemoved: string; + address: string; + kind: string; + parameters: WrapperContractEventParameters; +} + +export enum OrderEventEndState { + Invalid = 'INVALID', + Added = 'ADDED', + Filled = 'FILLED', + FullyFilled = 'FULLY_FILLED', + Cancelled = 'CANCELLED', + Expired = 'EXPIRED', + Unfunded = 'UNFUNDED', + FillabilityIncreased = 'FILLABILITY_INCREASED', +} + interface WrapperOrderEvent { orderHash: string; signedOrder: WrapperSignedOrder; - kind: string; + endState: OrderEventEndState; fillableTakerAssetAmount: string; - txHashes: string[]; + contractEvents: WrapperContractEvent[]; } /** @@ -141,9 +313,9 @@ interface WrapperOrderEvent { export interface OrderEvent { orderHash: string; signedOrder: SignedOrder; - kind: string; + endState: OrderEventEndState; fillableTakerAssetAmount: BigNumber; - txHashes: string[]; + contractEvents: ContractEvent[]; } // The type for validation results exposed by MeshWrapper. @@ -367,6 +539,107 @@ function wrapperSignedOrderToSignedOrder(wrapperSignedOrder: WrapperSignedOrder) }; } +function wrapperContractEventsToContractEvents(wrapperContractEvents: WrapperContractEvent[]): ContractEvent[] { + const contractEvents: ContractEvent[] = []; + if (wrapperContractEvents === null) { + return contractEvents; + } + wrapperContractEvents.forEach(wrapperContractEvent => { + const kind = wrapperContractEvent.kind as ContractEventKind; + const rawParameters = wrapperContractEvent.parameters; + let parameters: ContractEventParameters; + switch (kind) { + case ContractEventKind.ERC20TransferEvent: + const erc20TransferEvent = rawParameters as WrapperERC20TransferEvent; + parameters = { + from: erc20TransferEvent.from, + to: erc20TransferEvent.to, + value: new BigNumber(erc20TransferEvent.value), + }; + break; + case ContractEventKind.ERC20ApprovalEvent: + const erc20ApprovalEvent = rawParameters as WrapperERC20ApprovalEvent; + parameters = { + owner: erc20ApprovalEvent.owner, + spender: erc20ApprovalEvent.spender, + value: new BigNumber(erc20ApprovalEvent.value), + }; + break; + case ContractEventKind.ERC721TransferEvent: + const erc721TransferEvent = rawParameters as WrapperERC721TransferEvent; + parameters = { + from: erc721TransferEvent.from, + to: erc721TransferEvent.to, + tokenId: new BigNumber(erc721TransferEvent.tokenId), + }; + break; + case ContractEventKind.ERC721ApprovalEvent: + const erc721ApprovalEvent = rawParameters as WrapperERC721ApprovalEvent; + parameters = { + owner: erc721ApprovalEvent.owner, + approved: erc721ApprovalEvent.approved, + tokenId: new BigNumber(erc721ApprovalEvent.tokenId), + }; + break; + case ContractEventKind.ExchangeFillEvent: + const exchangeFillEvent = rawParameters as WrapperExchangeFillEvent; + parameters = { + makerAddress: exchangeFillEvent.makerAddress, + takerAddress: exchangeFillEvent.takerAddress, + senderAddress: exchangeFillEvent.senderAddress, + feeRecipientAddress: exchangeFillEvent.feeRecipientAddress, + makerAssetFilledAmount: new BigNumber(exchangeFillEvent.makerAssetFilledAmount), + takerAssetFilledAmount: new BigNumber(exchangeFillEvent.takerAssetFilledAmount), + makerFeePaid: new BigNumber(exchangeFillEvent.makerFeePaid), + takerFeePaid: new BigNumber(exchangeFillEvent.takerFeePaid), + orderHash: exchangeFillEvent.orderHash, + makerAssetData: exchangeFillEvent.makerAssetData, + takerAssetData: exchangeFillEvent.takerAssetData, + }; + break; + case ContractEventKind.ExchangeCancelEvent: + parameters = rawParameters as ExchangeCancelEvent; + break; + case ContractEventKind.ExchangeCancelUpToEvent: + const exchangeCancelUpToEvent = rawParameters as WrapperExchangeCancelUpToEvent; + parameters = { + makerAddress: exchangeCancelUpToEvent.makerAddress, + senderAddress: exchangeCancelUpToEvent.senderAddress, + orderEpoch: new BigNumber(exchangeCancelUpToEvent.orderEpoch), + }; + break; + case ContractEventKind.WethDepositEvent: + const wethDepositEvent = rawParameters as WrapperWethDepositEvent; + parameters = { + owner: wethDepositEvent.owner, + value: new BigNumber(wethDepositEvent.value), + }; + break; + case ContractEventKind.WethWithdrawalEvent: + const wethWithdrawalEvent = rawParameters as WrapperWethWithdrawalEvent; + parameters = { + owner: wethWithdrawalEvent.owner, + value: new BigNumber(wethWithdrawalEvent.value), + }; + break; + default: + throw new Error(`Unrecognized ContractEventKind: ${kind}`); + } + const contractEvent: ContractEvent = { + blockHash: wrapperContractEvent.blockHash, + txHash: wrapperContractEvent.txHash, + txIndex: wrapperContractEvent.txIndex, + logIndex: wrapperContractEvent.logIndex, + isRemoved: wrapperContractEvent.isRemoved, + address: wrapperContractEvent.address, + kind, + parameters, + }; + contractEvents.push(contractEvent); + }); + return contractEvents; + } + function signedOrderToWrapperSignedOrder(signedOrder: SignedOrder): WrapperSignedOrder { return { ...signedOrder, @@ -384,6 +657,7 @@ function wrapperOrderEventToOrderEvent(wrapperOrderEvent: WrapperOrderEvent): Or ...wrapperOrderEvent, signedOrder: wrapperSignedOrderToSignedOrder(wrapperOrderEvent.signedOrder), fillableTakerAssetAmount: new BigNumber(wrapperOrderEvent.fillableTakerAssetAmount), + contractEvents: wrapperContractEventsToContractEvents(wrapperOrderEvent.contractEvents), }; } diff --git a/core/validation.go b/core/validation.go index 47418e796..a06e80e81 100644 --- a/core/validation.go +++ b/core/validation.go @@ -10,6 +10,7 @@ import ( "github.com/0xProject/0x-mesh/p2p" "github.com/0xProject/0x-mesh/zeroex" "github.com/0xProject/0x-mesh/zeroex/ordervalidator" + "github.com/ethereum/go-ethereum/rpc" log "github.com/sirupsen/logrus" "github.com/xeipuuv/gojsonschema" ) @@ -192,7 +193,7 @@ func (app *App) validateOrders(orders []*zeroex.SignedOrder) (*ordervalidator.Va validMeshOrders = append(validMeshOrders, order) } areNewOrders := true - zeroexResults := app.orderValidator.BatchValidate(validMeshOrders, areNewOrders) + zeroexResults := app.orderValidator.BatchValidate(validMeshOrders, areNewOrders, rpc.LatestBlockNumber) zeroexResults.Accepted = append(zeroexResults.Accepted, results.Accepted...) zeroexResults.Rejected = append(zeroexResults.Rejected, results.Rejected...) return zeroexResults, nil diff --git a/docs/db_syncing.md b/docs/db_syncing.md index 8c0278b02..3050b9e18 100644 --- a/docs/db_syncing.md +++ b/docs/db_syncing.md @@ -14,9 +14,9 @@ When first connecting the DB and Mesh node, we first need to make sure both have #### 1. Subscribe to Mesh -Subscribe to the Mesh node's `orders` subscription over a WS connection. This can be done using our [golang](https://godoc.org/github.com/0xProject/0x-mesh/rpc) or [Typescript/Javascript](json_rpc_clients/typescript/README.md) clients or any other JSON-RPC WebSocket client. Whenever you receive an order event from this subscription, make the appropriate updates to your DB. Each order event has an associated [OrderEventKind](https://godoc.org/github.com/0xProject/0x-mesh/zeroex#pkg-constants). +Subscribe to the Mesh node's `orders` subscription over a WS connection. This can be done using our [golang](https://godoc.org/github.com/0xProject/0x-mesh/rpc) or [Typescript/Javascript](json_rpc_clients/typescript/README.md) clients or any other JSON-RPC WebSocket client. Whenever you receive an order event from this subscription, make the appropriate updates to your DB. Each order event has an associated [OrderEventEndState](https://godoc.org/github.com/0xProject/0x-mesh/zeroex#pkg-constants). -| Kind | DB operation | +| End state | DB operation | |--------------------------------------------|---------------------------------| | ADDED | Insert | | FILLED | Update | @@ -25,6 +25,8 @@ Subscribe to the Mesh node's `orders` subscription over a WS connection. This ca **Note:** Updates refer to updating the order's `fillableTakerAssetAmount` in the DB. +**Note 2:** If we receive any event other than `ADDED` and `FILLABILITY_INCREASED` for an order we do not find in our database, we ignore the event and noop. + #### 2. Get all orders currently stored in Mesh There might have been orders stored in Mesh that the DB doesn't know about at this time. Because of this, we must fetch all currently stored orders in the Mesh node and upsert them in the database. This can be done using the [mesh_getOrders](rpc_api.md#mesh_getorders) JSON-RPC method. This method creates a snapshot of the Mesh node's internal DB of orders when first called, and allows for subsequent paginated requests against this snapshot. Because we are already subscribed to order events, any new orders added/removed after the snapshot is made will be discovered via that subscription. diff --git a/docs/rpc_api.md b/docs/rpc_api.md index f8c2fae71..cde7f440f 100644 --- a/docs/rpc_api.md +++ b/docs/rpc_api.md @@ -211,9 +211,12 @@ Gets certain configurations and stats about a Mesh node. ### `mesh_subscribe` to `orders` topic -Allows the caller to subscribe to a stream of `OrderEvents`. An `OrderEvent` contains either newly discovered orders found by Mesh via the P2P network, or updates to the fillability of a previously discovered order (e.g., if an order gets filled, cancelled, expired, etc...). `OrderEvent`s _do not_ correspond 1-to-1 to smart contract events. Rather, an `OrderEvent` about an orders fillability change represents the aggregate change to it's fillability given _all_ the transactions included within the most recently mined block. +Allows the caller to subscribe to a stream of `OrderEvents`. An `OrderEvent` contains either newly discovered orders found by Mesh via the P2P network, or updates to the fillability of a previously discovered order (e.g., if an order gets filled, cancelled, expired, etc...). `OrderEvent`s _do not_ correspond 1-to-1 to smart contract events. Rather, an `OrderEvent` about an orders fillability change represents the aggregate change to it's fillability given _all_ the transactions included within the most recently mined/reverted blocks. -**Example:** If an order is both `filled` and `cancelled` within a single block, only a cancellation `OrderEvent` will be emitted (since this is the final state of the order after this block is mined). The cancellation `OrderEvent` _will_ however list the hashes of all transactions that impacted the order's fillability within the block. Using these `txHashes`, one can fetch all the individual smart contract events impacting to an order if they are needed. +**Example:** If an order is both `filled` and `cancelled` within a single block, the `EndState` +of the `OrderEvent` will be `CANCELLED` (since this is the final state of the order after this block is +mined). The `OrderEvent` _will_ however list the contract events intercepted that could have impacted +this orders fillability. This list will include both the fill event and cancellation event. Mesh has implemented subscriptions in the [same manner as Geth](https://github.com/ethereum/go-ethereum/wiki/RPC-PUB-SUB). In order to start a subscription, you must send the following payload: @@ -265,16 +268,34 @@ Mesh has implemented subscriptions in the [same manner as Geth](https://github.c "salt": "1559422141994", "signature": "0x1cf16c2f3a210965b5e17f51b57b869ba4ddda33df92b0017b4d8da9dacd3152b122a73844eaf50ccde29a42950239ba36a525ed7f1698a8a5e1896cf7d651aed203" }, - "kind": "CANCELLED", + "endState": "CANCELLED", "fillableTakerAssetAmount": 0, - "txHashes": ["0x9e6830a7044b39e107f410e4f765995fd0d3d69d5c3b3582a1701b9d68167560"] + "contractEvents": [ + { + "blockHash": "0x1be2eb6174dbf0458686bdae44c9a330d9a9eb563962512a7be545c4ec11a4d2", + "txHash": "0xbcce172374dbf0458686bdae44c9a330d9a9eb563962512a7be545c4ec232e3a", + "txIndex": 23, + "logIndex": 0, + "isRemoved": false, + "address": "0x4f833a24e1f95d70f028921e27040ca56e09ab0b", + "kind": "ExchangeCancelEvent", + "parameters": { + "makerAddress": "0x50f84bbee6fb250d6f49e854fa280445369d64d9", + "senderAddress": "0x0000000000000000000000000000000000000000", + "feeRecipientAddress": "0xa258b39954cef5cb142fd567a46cddb31a670124", + "orderHash": "0x96e6eb6174dbf0458686bdae44c9a330d9a9eb563962512a7be545c4ecc13fd4", + "makerAssetData": "0xf47261b00000000000000000000000000f5d2fb29fb7d3cfee444a200298f468908cc942", + "takerAssetData": "0xf47261b0000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" + } + } + ] } ] } } ``` -See the [OrderEvent](https://godoc.org/github.com/0xProject/0x-mesh/zeroex#OrderEvent) type declaration as well as the [EventKind](https://godoc.org/github.com/0xProject/0x-mesh/zeroex#pkg-constants) event types for a complete list of the events that could be emitted. +See the [OrderEvent](https://godoc.org/github.com/0xProject/0x-mesh/zeroex#OrderEvent) type declaration as well as the [OrderEventEndState](https://godoc.org/github.com/0xProject/0x-mesh/zeroex#pkg-constants) types for a complete list of the events that could be emitted. To unsubscribe, send a `mesh_unsubscribe` request specifying the `subscriptionId`. diff --git a/integration-tests/integration_test.go b/integration-tests/integration_test.go index 795b98905..8da4c2d27 100644 --- a/integration-tests/integration_test.go +++ b/integration-tests/integration_test.go @@ -196,7 +196,7 @@ func TestBrowserIntegration(t *testing.T) { // Next, wait for the order to be received. expectedOrderEventLog := orderEventLog{ OrderHash: standaloneOrderHash.Hex(), - Kind: "ADDED", + EndState: "ADDED", } _, err = waitForOrderEventLog(ctx, browserLogMessages, expectedOrderEventLog) assert.NoError(t, err, "Browser node did not receive order sent by standalone node") @@ -245,6 +245,12 @@ func buildForTests(t *testing.T, ctx context.Context) { output, err = cmd.CombinedOutput() require.NoError(t, err, "could not build mesh-bootstrap: %s", string(output)) + fmt.Println("Clear yarn cache...") + cmd = exec.CommandContext(ctx, "yarn", "cache", "clean") + cmd.Dir = "../browser" + output, err = cmd.CombinedOutput() + require.NoError(t, err, "could not clean yarn cache: %s", string(output)) + fmt.Println("Installing dependencies for TypeScript bindings...") cmd = exec.CommandContext(ctx, "yarn", "install") cmd.Dir = "../browser" @@ -419,7 +425,7 @@ func waitForReceivedOrderLog(ctx context.Context, logMessages <-chan string, exp // by Mesh. They need to be explicitly logged. type orderEventLog struct { OrderHash string `json:"orderHash"` - Kind string `json:"kind"` + EndState string `json:"endState"` } func waitForOrderEventLog(ctx context.Context, logMessages <-chan string, expectedLog orderEventLog) (string, error) { @@ -429,7 +435,7 @@ func waitForOrderEventLog(ctx context.Context, logMessages <-chan string, expect return false } return foundLog.OrderHash == expectedLog.OrderHash && - foundLog.Kind == expectedLog.Kind + foundLog.EndState == expectedLog.EndState }) } diff --git a/rpc/clients/typescript/src/index.ts b/rpc/clients/typescript/src/index.ts index f64224fb4..17e860b98 100644 --- a/rpc/clients/typescript/src/index.ts +++ b/rpc/clients/typescript/src/index.ts @@ -2,7 +2,7 @@ export { WSClient } from './ws_client'; export { ClientConfig, WSOpts, - OrderEventKind, + OrderEventEndState, OrderEventPayload, OrderEvent, OrderInfo, diff --git a/rpc/clients/typescript/src/types.ts b/rpc/clients/typescript/src/types.ts index 8097aa6ee..f4c2333d2 100644 --- a/rpc/clients/typescript/src/types.ts +++ b/rpc/clients/typescript/src/types.ts @@ -50,7 +50,168 @@ export interface StringifiedSignedOrder { signature: string; } -export enum OrderEventKind { +export interface ERC20TransferEvent { + from: string; + to: string; + value: BigNumber; +} + +export interface StringifiedERC20TransferEvent { + from: string; + to: string; + value: string; +} + +export interface ERC20ApprovalEvent { + owner: string; + spender: string; + value: BigNumber; +} + +export interface StringifiedERC20ApprovalEvent { + owner: string; + spender: string; + value: string; +} + +export interface ERC721TransferEvent { + from: string; + to: string; + tokenId: BigNumber; +} + +export interface StringifiedERC721TransferEvent { + from: string; + to: string; + tokenId: string; +} + +export interface ERC721ApprovalEvent { + owner: string; + approved: string; + tokenId: BigNumber; +} + +export interface StringifiedERC721ApprovalEvent { + owner: string; + approved: string; + tokenId: string; +} + +export interface ERC721ApprovalForAllEvent { + owner: string; + operator: string; + approved: boolean; +} + +export interface ExchangeFillEvent { + makerAddress: string; + takerAddress: string; + senderAddress: string; + feeRecipientAddress: string; + makerAssetFilledAmount: BigNumber; + takerAssetFilledAmount: BigNumber; + makerFeePaid: BigNumber; + takerFeePaid: BigNumber; + orderHash: string; + makerAssetData: string; + takerAssetData: string; +} + +export interface StringifiedExchangeFillEvent { + makerAddress: string; + takerAddress: string; + senderAddress: string; + feeRecipientAddress: string; + makerAssetFilledAmount: string; + takerAssetFilledAmount: string; + makerFeePaid: string; + takerFeePaid: string; + orderHash: string; + makerAssetData: string; + takerAssetData: string; +} + +export interface ExchangeCancelEvent { + makerAddress: string; + senderAddress: string; + feeRecipientAddress: string; + orderHash: string; + makerAssetData: string; + takerAssetData: string; +} + +export interface ExchangeCancelUpToEvent { + makerAddress: string; + senderAddress: string; + orderEpoch: BigNumber; +} + +export interface StringifiedExchangeCancelUpToEvent { + makerAddress: string; + senderAddress: string; + orderEpoch: string; +} + +export interface WethWithdrawalEvent { + owner: string; + value: BigNumber; +} + +export interface StringifiedWethWithdrawalEvent { + owner: string; + value: string; +} + +export interface WethDepositEvent { + owner: string; + value: BigNumber; +} + +export interface StringifiedWethDepositEvent { + owner: string; + value: string; +} + +export enum ContractEventKind { + ERC20TransferEvent = 'ERC20TransferEvent', + ERC20ApprovalEvent = 'ERC20ApprovalEvent', + ERC721TransferEvent = 'ERC721TransferEvent', + ERC721ApprovalEvent = 'ERC721ApprovalEvent', + ExchangeFillEvent = 'ExchangeFillEvent', + ExchangeCancelEvent = 'ExchangeCancelEvent', + ExchangeCancelUpToEvent = 'ExchangeCancelUpToEvent', + WethDepositEvent = 'WethDepositEvent', + WethWithdrawalEvent = 'WethWithdrawalEvent', +} + +export type StringifiedContractEventParameters = StringifiedERC20TransferEvent | StringifiedERC20ApprovalEvent | StringifiedERC721TransferEvent | StringifiedERC721ApprovalEvent | StringifiedExchangeFillEvent | StringifiedExchangeCancelUpToEvent | StringifiedWethWithdrawalEvent | StringifiedWethDepositEvent | ERC721ApprovalForAllEvent | ExchangeCancelEvent; + +export interface StringifiedContractEvent { + blockHash: string; + txHash: string; + txIndex: number; + logIndex: number; + isRemoved: string; + address: string; + kind: string; + parameters: StringifiedContractEventParameters; +} + +export type ContractEventParameters = ERC20TransferEvent | ERC20ApprovalEvent | ERC721TransferEvent | ERC721ApprovalEvent | ExchangeFillEvent | ExchangeCancelUpToEvent | WethWithdrawalEvent | WethDepositEvent | ERC721ApprovalForAllEvent | ExchangeCancelEvent; + +export interface ContractEvent { + blockHash: string; + txHash: string; + txIndex: number; + logIndex: number; + isRemoved: string; + address: string; + kind: ContractEventKind; + parameters: ContractEventParameters; +} + +export enum OrderEventEndState { Invalid = 'INVALID', Added = 'ADDED', Filled = 'FILLED', @@ -74,17 +235,17 @@ export interface HeartbeatEventPayload { export interface RawOrderEvent { orderHash: string; signedOrder: StringifiedSignedOrder; - kind: OrderEventKind; + endState: OrderEventEndState; fillableTakerAssetAmount: string; - txHashes: string[]; + contractEvents: StringifiedContractEvent[]; } export interface OrderEvent { orderHash: string; signedOrder: SignedOrder; - kind: OrderEventKind; + endState: OrderEventEndState; fillableTakerAssetAmount: BigNumber; - txHashes: string[]; + contractEvents: ContractEvent[]; } export interface RawAcceptedOrderInfo { diff --git a/rpc/clients/typescript/src/ws_client.ts b/rpc/clients/typescript/src/ws_client.ts index f27bb797c..7c653547e 100644 --- a/rpc/clients/typescript/src/ws_client.ts +++ b/rpc/clients/typescript/src/ws_client.ts @@ -8,6 +8,10 @@ import * as WebSocket from 'websocket'; import { AcceptedOrderInfo, + ContractEvent, + ContractEventKind, + ContractEventParameters, + ExchangeCancelEvent, GetOrdersResponse, GetStatsResponse, HeartbeatEventPayload, @@ -19,6 +23,15 @@ import { RawOrderInfo, RawValidationResults, RejectedOrderInfo, + StringifiedContractEvent, + StringifiedERC20ApprovalEvent, + StringifiedERC20TransferEvent, + StringifiedERC721ApprovalEvent, + StringifiedERC721TransferEvent, + StringifiedExchangeCancelUpToEvent, + StringifiedExchangeFillEvent, + StringifiedWethDepositEvent, + StringifiedWethWithdrawalEvent, ValidationResults, WSOpts, } from './types'; @@ -69,6 +82,106 @@ export class WSClient { }); return orderInfos; } + private static _convertStringifiedContractEvents(rawContractEvents: StringifiedContractEvent[]): ContractEvent[] { + const contractEvents: ContractEvent[] = []; + if (rawContractEvents === null) { + return contractEvents; + } + rawContractEvents.forEach(rawContractEvent => { + const kind = rawContractEvent.kind as ContractEventKind; + const rawParameters = rawContractEvent.parameters; + let parameters: ContractEventParameters; + switch (kind) { + case ContractEventKind.ERC20TransferEvent: + const erc20TransferEvent = rawParameters as StringifiedERC20TransferEvent; + parameters = { + from: erc20TransferEvent.from, + to: erc20TransferEvent.to, + value: new BigNumber(erc20TransferEvent.value), + }; + break; + case ContractEventKind.ERC20ApprovalEvent: + const erc20ApprovalEvent = rawParameters as StringifiedERC20ApprovalEvent; + parameters = { + owner: erc20ApprovalEvent.owner, + spender: erc20ApprovalEvent.spender, + value: new BigNumber(erc20ApprovalEvent.value), + }; + break; + case ContractEventKind.ERC721TransferEvent: + const erc721TransferEvent = rawParameters as StringifiedERC721TransferEvent; + parameters = { + from: erc721TransferEvent.from, + to: erc721TransferEvent.to, + tokenId: new BigNumber(erc721TransferEvent.tokenId), + }; + break; + case ContractEventKind.ERC721ApprovalEvent: + const erc721ApprovalEvent = rawParameters as StringifiedERC721ApprovalEvent; + parameters = { + owner: erc721ApprovalEvent.owner, + approved: erc721ApprovalEvent.approved, + tokenId: new BigNumber(erc721ApprovalEvent.tokenId), + }; + break; + case ContractEventKind.ExchangeFillEvent: + const exchangeFillEvent = rawParameters as StringifiedExchangeFillEvent; + parameters = { + makerAddress: exchangeFillEvent.makerAddress, + takerAddress: exchangeFillEvent.takerAddress, + senderAddress: exchangeFillEvent.senderAddress, + feeRecipientAddress: exchangeFillEvent.feeRecipientAddress, + makerAssetFilledAmount: new BigNumber(exchangeFillEvent.makerAssetFilledAmount), + takerAssetFilledAmount: new BigNumber(exchangeFillEvent.takerAssetFilledAmount), + makerFeePaid: new BigNumber(exchangeFillEvent.makerFeePaid), + takerFeePaid: new BigNumber(exchangeFillEvent.takerFeePaid), + orderHash: exchangeFillEvent.orderHash, + makerAssetData: exchangeFillEvent.makerAssetData, + takerAssetData: exchangeFillEvent.takerAssetData, + }; + break; + case ContractEventKind.ExchangeCancelEvent: + parameters = rawParameters as ExchangeCancelEvent; + break; + case ContractEventKind.ExchangeCancelUpToEvent: + const exchangeCancelUpToEvent = rawParameters as StringifiedExchangeCancelUpToEvent; + parameters = { + makerAddress: exchangeCancelUpToEvent.makerAddress, + senderAddress: exchangeCancelUpToEvent.senderAddress, + orderEpoch: new BigNumber(exchangeCancelUpToEvent.orderEpoch), + }; + break; + case ContractEventKind.WethDepositEvent: + const wethDepositEvent = rawParameters as StringifiedWethDepositEvent; + parameters = { + owner: wethDepositEvent.owner, + value: new BigNumber(wethDepositEvent.value), + }; + break; + case ContractEventKind.WethWithdrawalEvent: + const wethWithdrawalEvent = rawParameters as StringifiedWethWithdrawalEvent; + parameters = { + owner: wethWithdrawalEvent.owner, + value: new BigNumber(wethWithdrawalEvent.value), + }; + break; + default: + throw new Error(`Unrecognized ContractEventKind: ${kind}`); + } + const contractEvent: ContractEvent = { + blockHash: rawContractEvent.blockHash, + txHash: rawContractEvent.txHash, + txIndex: rawContractEvent.txIndex, + logIndex: rawContractEvent.logIndex, + isRemoved: rawContractEvent.isRemoved, + address: rawContractEvent.address, + kind, + parameters, + }; + contractEvents.push(contractEvent); + }); + return contractEvents; + } /** * Instantiates a new WSClient instance * @param url WS server endpoint @@ -167,9 +280,9 @@ export class WSClient { const orderEvent = { orderHash: rawOrderEvent.orderHash, signedOrder: orderParsingUtils.convertOrderStringFieldsToBigNumber(rawOrderEvent.signedOrder), - kind: rawOrderEvent.kind, + endState: rawOrderEvent.endState, fillableTakerAssetAmount: new BigNumber(rawOrderEvent.fillableTakerAssetAmount), - txHashes: rawOrderEvent.txHashes, + contractEvents: WSClient._convertStringifiedContractEvents(rawOrderEvent.contractEvents), }; orderEvents.push(orderEvent); }); diff --git a/rpc/clients/typescript/test/ws_client_test.ts b/rpc/clients/typescript/test/ws_client_test.ts index 0f868b3ca..cf6c1a381 100644 --- a/rpc/clients/typescript/test/ws_client_test.ts +++ b/rpc/clients/typescript/test/ws_client_test.ts @@ -321,9 +321,27 @@ describe('WSClient', () => { "salt": "1559422141994", "signature": "0x1cf16c2f3a210965b5e17f51b57b869ba4ddda33df92b0017b4d8da9dacd3152b122a73844eaf50ccde29a42950239ba36a525ed7f1698a8a5e1896cf7d651aed203" }, - "kind": "CANCELLED", + "endState": "CANCELLED", "fillableTakerAssetAmount": 0, - "txHashes": ["0x9e6830a7044b39e107f410e4f765995fd0d3d69d5c3b3582a1701b9d68167560"] + "contractEvents": [ + { + "blockHash": "0x1be2eb6174dbf0458686bdae44c9a330d9a9eb563962512a7be545c4ec11a4d2", + "txHash": "0xbcce172374dbf0458686bdae44c9a330d9a9eb563962512a7be545c4ec232e3a", + "txIndex": 23, + "logIndex": 0, + "isRemoved": false, + "address": "0x4f833a24e1f95d70f028921e27040ca56e09ab0b", + "kind": "ExchangeCancelEvent", + "parameters": { + "makerAddress": "0x50f84bbee6fb250d6f49e854fa280445369d64d9", + "senderAddress": "0x0000000000000000000000000000000000000000", + "feeRecipientAddress": "0xa258b39954cef5cb142fd567a46cddb31a670124", + "orderHash": "0x96e6eb6174dbf0458686bdae44c9a330d9a9eb563962512a7be545c4ecc13fd4", + "makerAssetData": "0xf47261b00000000000000000000000000f5d2fb29fb7d3cfee444a200298f468908cc942", + "takerAssetData": "0xf47261b0000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2" + } + } + ] } ] } diff --git a/zeroex/order.go b/zeroex/order.go index 6a9a761f2..904b13f74 100644 --- a/zeroex/order.go +++ b/zeroex/order.go @@ -74,38 +74,67 @@ const ( OSInvalidTakerAssetData ) +// ContractEventParameters is the parameters of a ContractEvent +type ContractEventParameters interface { + json.Marshaler +} + +// ContractEvent is an event emitted by a smart contract +type ContractEvent struct { + BlockHash common.Hash + TxHash common.Hash + TxIndex uint + LogIndex uint + IsRemoved bool + Address common.Address + Kind string + Parameters ContractEventParameters +} + +// MarshalJSON implements a custom JSON marshaller for the ContractEvent type +func (c ContractEvent) MarshalJSON() ([]byte, error) { + m := map[string]interface{}{ + "blockHash": c.BlockHash.Hex(), + "txHash": c.TxHash.Hex(), + "txIndex": c.TxIndex, + "logIndex": c.LogIndex, + "isRemoved": c.IsRemoved, + "address": c.Address, + "kind": c.Kind, + "parameters": c.Parameters, + } + return json.Marshal(m) +} + // OrderEvent is the order event emitted by Mesh nodes on the "orders" topic // when calling JSON-RPC method `mesh_subscribe` type OrderEvent struct { - OrderHash common.Hash `json:"orderHash"` - SignedOrder *SignedOrder `json:"signedOrder"` - Kind OrderEventKind `json:"kind"` - FillableTakerAssetAmount *big.Int `json:"fillableTakerAssetAmount"` - // The hashes of the Ethereum transactions that caused the order status to change. - // Could be because of multiple transactions, not just a single transaction. - TxHashes []common.Hash `json:"txHashes"` + OrderHash common.Hash `json:"orderHash"` + SignedOrder *SignedOrder `json:"signedOrder"` + EndState OrderEventEndState `json:"endState"` + FillableTakerAssetAmount *big.Int `json:"fillableTakerAssetAmount"` + // All the contract events that triggered this orders re-evaluation. They did not + // all necessarily cause the orders state change itself, only it's re-evaluation. + // Since it's state _did_ change, at least one of them did cause the actual state change. + ContractEvents []*ContractEvent `json:"contractEvents"` } type orderEventJSON struct { - OrderHash string `json:"orderHash"` - SignedOrder *SignedOrder `json:"signedOrder"` - Kind string `json:"kind"` - FillableTakerAssetAmount string `json:"fillableTakerAssetAmount"` - TxHashes []string `json:"txHashes"` + OrderHash string `json:"orderHash"` + SignedOrder *SignedOrder `json:"signedOrder"` + EndState string `json:"endState"` + FillableTakerAssetAmount string `json:"fillableTakerAssetAmount"` + ContractEvents []*ContractEvent `json:"contractEvents"` } // MarshalJSON implements a custom JSON marshaller for the OrderEvent type func (o OrderEvent) MarshalJSON() ([]byte, error) { - stringifiedTxHashes := []string{} - for _, txHash := range o.TxHashes { - stringifiedTxHashes = append(stringifiedTxHashes, txHash.Hex()) - } return json.Marshal(map[string]interface{}{ "orderHash": o.OrderHash.Hex(), "signedOrder": o.SignedOrder, - "kind": o.Kind, + "endState": o.EndState, "fillableTakerAssetAmount": o.FillableTakerAssetAmount.String(), - "txHashes": stringifiedTxHashes, + "contractEvents": o.ContractEvents, }) } @@ -122,37 +151,34 @@ func (o *OrderEvent) UnmarshalJSON(data []byte) error { func (o *OrderEvent) fromOrderEventJSON(orderEventJSON orderEventJSON) error { o.OrderHash = common.HexToHash(orderEventJSON.OrderHash) o.SignedOrder = orderEventJSON.SignedOrder - o.Kind = OrderEventKind(orderEventJSON.Kind) + o.EndState = OrderEventEndState(orderEventJSON.EndState) var ok bool o.FillableTakerAssetAmount, ok = math.ParseBig256(orderEventJSON.FillableTakerAssetAmount) if !ok { return errors.New("Invalid uint256 number encountered for FillableTakerAssetAmount") } - txHashes := []common.Hash{} - for _, txHash := range orderEventJSON.TxHashes { - txHashes = append(txHashes, common.HexToHash(txHash)) - } - o.TxHashes = txHashes + o.ContractEvents = orderEventJSON.ContractEvents return nil } -// OrderEventKind enumerates all the possible order event types -type OrderEventKind string +// OrderEventEndState enumerates all the possible order event types. An OrderEventEndState describes the +// end state of a 0x order after revalidation +type OrderEventEndState string -// OrderEventKind values +// OrderEventEndState values const ( - EKInvalid = OrderEventKind("INVALID") - EKOrderAdded = OrderEventKind("ADDED") - EKOrderFilled = OrderEventKind("FILLED") - EKOrderFullyFilled = OrderEventKind("FULLY_FILLED") - EKOrderCancelled = OrderEventKind("CANCELLED") - EKOrderExpired = OrderEventKind("EXPIRED") + ESInvalid = OrderEventEndState("INVALID") + ESOrderAdded = OrderEventEndState("ADDED") + ESOrderFilled = OrderEventEndState("FILLED") + ESOrderFullyFilled = OrderEventEndState("FULLY_FILLED") + ESOrderCancelled = OrderEventEndState("CANCELLED") + ESOrderExpired = OrderEventEndState("EXPIRED") // An order becomes unfunded if the maker transfers the balance / changes their // allowance backing an order - EKOrderBecameUnfunded = OrderEventKind("UNFUNDED") + ESOrderBecameUnfunded = OrderEventEndState("UNFUNDED") // Fillability for an order can increase if a previously processed fill event // gets reverted, or if a maker tops up their balance/allowance backing an order - EKOrderFillabilityIncreased = OrderEventKind("FILLABILITY_INCREASED") + ESOrderFillabilityIncreased = OrderEventEndState("FILLABILITY_INCREASED") ) var eip712OrderTypes = signer.Types{ diff --git a/zeroex/order_js.go b/zeroex/order_js.go index fdc421f01..45fc320ca 100644 --- a/zeroex/order_js.go +++ b/zeroex/order_js.go @@ -6,21 +6,20 @@ import ( "fmt" "strings" "syscall/js" - "github.com/ethereum/go-ethereum/common" ) func (o OrderEvent) JSValue() js.Value { - stringifiedTxHashes := []interface{}{} - for _, txHash := range o.TxHashes { - stringifiedTxHashes = append(stringifiedTxHashes, txHash.Hex()) + contractEventsJS := make([]interface{}, len(o.ContractEvents)) + for i, contractEvent := range o.ContractEvents { + contractEventsJS[i] = contractEvent.JSValue() } return js.ValueOf(map[string]interface{}{ "orderHash": o.OrderHash.Hex(), "signedOrder": o.SignedOrder.JSValue(), - "kind": string(o.Kind), + "endState": string(o.EndState), "fillableTakerAssetAmount": o.FillableTakerAssetAmount.String(), - "txHashes": stringifiedTxHashes, + "contractEvents": contractEventsJS, }) } @@ -55,3 +54,16 @@ func (s SignedOrder) JSValue() js.Value { "signature": signature, }) } + +func (c ContractEvent) JSValue() js.Value { + m := map[string]interface{}{ + "blockHash": c.BlockHash.Hex(), + "txHash": c.TxHash.Hex(), + "txIndex": c.TxIndex, + "logIndex": c.LogIndex, + "isRemoved": c.IsRemoved, + "kind": c.Kind, + "parameters": c.Parameters.(js.Wrapper).JSValue(), + } + return js.ValueOf(m) +} diff --git a/zeroex/order_test.go b/zeroex/order_test.go index 760171f37..173f02596 100644 --- a/zeroex/order_test.go +++ b/zeroex/order_test.go @@ -56,9 +56,9 @@ func TestMarshalUnmarshalOrderEvent(t *testing.T) { orderEvent := OrderEvent{ OrderHash: orderHash, SignedOrder: signedOrder, - Kind: EKOrderAdded, + EndState: ESOrderAdded, FillableTakerAssetAmount: big.NewInt(2000), - TxHashes: []common.Hash{common.HexToHash("0x3fcd58a6613265e2b0deba902d7ff693f330a0af6e5b04805b44bbffd8a415d3")}, + ContractEvents: []*ContractEvent{}, } buf := &bytes.Buffer{} diff --git a/zeroex/ordervalidator/order_validator.go b/zeroex/ordervalidator/order_validator.go index 9263d3eda..744cbb16f 100644 --- a/zeroex/ordervalidator/order_validator.go +++ b/zeroex/ordervalidator/order_validator.go @@ -22,6 +22,7 @@ import ( "github.com/ethereum/go-ethereum/common/hexutil" "github.com/ethereum/go-ethereum/common/math" "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/rpc" "github.com/jpillora/backoff" log "github.com/sirupsen/logrus" ) @@ -185,20 +186,20 @@ var ( // ROInvalidSchemaCode is the RejectedOrderStatus emitted if an order doesn't conform to the order schema const ROInvalidSchemaCode = "InvalidSchema" -// ConvertRejectOrderCodeToOrderEventKind converts an RejectOrderCode to an OrderEventKind type -func ConvertRejectOrderCodeToOrderEventKind(rejectedOrderStatus RejectedOrderStatus) (zeroex.OrderEventKind, bool) { +// ConvertRejectOrderCodeToOrderEventEndState converts an RejectOrderCode to an OrderEventEndState type +func ConvertRejectOrderCodeToOrderEventEndState(rejectedOrderStatus RejectedOrderStatus) (zeroex.OrderEventEndState, bool) { switch rejectedOrderStatus { case ROExpired: - return zeroex.EKOrderExpired, true + return zeroex.ESOrderExpired, true case ROFullyFilled: - return zeroex.EKOrderFullyFilled, true + return zeroex.ESOrderFullyFilled, true case ROCancelled: - return zeroex.EKOrderCancelled, true + return zeroex.ESOrderCancelled, true case ROUnfunded: - return zeroex.EKOrderBecameUnfunded, true + return zeroex.ESOrderBecameUnfunded, true default: - // Catch-all returns Invalid OrderEventKind - return zeroex.EKInvalid, false + // Catch-all returns Invalid OrderEventEndState + return zeroex.ESInvalid, false } } @@ -271,9 +272,11 @@ func New(ethClient *ethclient.Client, networkID int, maxRequestContentLength int // BatchValidate retrieves all the information needed to validate the supplied orders. // It splits the orders into chunks of `chunkSize`, and makes no more then `concurrencyLimit` // requests concurrently. If a request fails, re-attempt it up to four times before giving up. -// If it some requests fail, this method still returns whatever order information it was able to -// retrieve. -func (o *OrderValidator) BatchValidate(rawSignedOrders []*zeroex.SignedOrder, areNewOrders bool) *ValidationResults { +// If some requests fail, this method still returns whatever order information it was able to +// retrieve up until the failure. +// The `blockNumber` parameter lets the caller specify a specific block height at which to validate +// the orders. This can be set to the `latest` block or any other historical block number. +func (o *OrderValidator) BatchValidate(rawSignedOrders []*zeroex.SignedOrder, areNewOrders bool, blockNumber rpc.BlockNumber) *ValidationResults { if len(rawSignedOrders) == 0 { return &ValidationResults{} } @@ -335,6 +338,11 @@ func (o *OrderValidator) BatchValidate(rawSignedOrders []*zeroex.SignedOrder, ar Pending: false, Context: ctx, } + if blockNumber == rpc.PendingBlockNumber { + opts.Pending = true + } else if blockNumber != rpc.LatestBlockNumber { + opts.BlockNumber = big.NewInt(int64(blockNumber)) + } results, err := o.devUtils.GetOrderRelevantStates(opts, orders, signatures) if err != nil { diff --git a/zeroex/ordervalidator/order_validator_test.go b/zeroex/ordervalidator/order_validator_test.go index 71d290299..020ee1b69 100644 --- a/zeroex/ordervalidator/order_validator_test.go +++ b/zeroex/ordervalidator/order_validator_test.go @@ -23,6 +23,7 @@ import ( "github.com/ethereum/go-ethereum/core/types" "github.com/ethereum/go-ethereum/ethclient" ethrpc "github.com/ethereum/go-ethereum/rpc" + "github.com/ethereum/go-ethereum/rpc" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -174,7 +175,7 @@ func TestBatchValidateAValidOrder(t *testing.T) { orderValidator, err := New(ethClient, constants.TestNetworkID, constants.TestMaxContentLength, 0) require.NoError(t, err) - validationResults := orderValidator.BatchValidate(signedOrders, areNewOrders) + validationResults := orderValidator.BatchValidate(signedOrders, areNewOrders, rpc.LatestBlockNumber) assert.Len(t, validationResults.Accepted, 1) require.Len(t, validationResults.Rejected, 0) orderHash, err := signedOrder.ComputeOrderHash() @@ -199,7 +200,7 @@ func TestBatchValidateSignatureInvalid(t *testing.T) { orderValidator, err := New(ethClient, constants.TestNetworkID, constants.TestMaxContentLength, 0) require.NoError(t, err) - validationResults := orderValidator.BatchValidate(signedOrders, areNewOrders) + validationResults := orderValidator.BatchValidate(signedOrders, areNewOrders, rpc.LatestBlockNumber) assert.Len(t, validationResults.Accepted, 0) require.Len(t, validationResults.Rejected, 1) assert.Equal(t, ROInvalidSignature, validationResults.Rejected[0].Status) @@ -224,7 +225,7 @@ func TestBatchValidateUnregisteredCoordinatorSoftCancels(t *testing.T) { orderValidator, err := New(ethClient, constants.TestNetworkID, constants.TestMaxContentLength, 0) require.NoError(t, err) - validationResults := orderValidator.BatchValidate(signedOrders, areNewOrders) + validationResults := orderValidator.BatchValidate(signedOrders, areNewOrders, rpc.LatestBlockNumber) assert.Len(t, validationResults.Accepted, 0) require.Len(t, validationResults.Rejected, 1) assert.Equal(t, ROCoordinatorEndpointNotFound, validationResults.Rejected[0].Status) @@ -276,7 +277,7 @@ func TestBatchValidateCoordinatorSoftCancels(t *testing.T) { _, err = orderValidator.coordinatorRegistry.SetCoordinatorEndpoint(opts, testServer.URL) require.NoError(t, err) - validationResults := orderValidator.BatchValidate(signedOrders, areNewOrders) + validationResults := orderValidator.BatchValidate(signedOrders, areNewOrders, rpc.LatestBlockNumber) assert.Len(t, validationResults.Accepted, 0) require.Len(t, validationResults.Rejected, 1) assert.Equal(t, ROCoordinatorSoftCancelled, validationResults.Rejected[0].Status) diff --git a/zeroex/orderwatch/event_decoder.go b/zeroex/orderwatch/decoder/event_decoder.go similarity index 81% rename from zeroex/orderwatch/event_decoder.go rename to zeroex/orderwatch/decoder/event_decoder.go index e01032af4..ec5fe18a9 100644 --- a/zeroex/orderwatch/event_decoder.go +++ b/zeroex/orderwatch/decoder/event_decoder.go @@ -1,6 +1,7 @@ -package orderwatch +package decoder import ( + "encoding/json" "errors" "fmt" "math/big" @@ -40,6 +41,15 @@ type ERC20TransferEvent struct { Value *big.Int } +// MarshalJSON implements a custom JSON marshaller for the ERC20TransferEvent type +func (e ERC20TransferEvent) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]interface{}{ + "from": e.From.Hex(), + "to": e.To.Hex(), + "value": e.Value.String(), + }) +} + // ERC20ApprovalEvent represents an ERC20 Approval event type ERC20ApprovalEvent struct { Owner common.Address @@ -47,6 +57,15 @@ type ERC20ApprovalEvent struct { Value *big.Int } +// MarshalJSON implements a custom JSON marshaller for the ERC20ApprovalEvent type +func (e ERC20ApprovalEvent) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]interface{}{ + "owner": e.Owner.Hex(), + "spender": e.Spender.Hex(), + "value": e.Value.String(), + }) +} + // ERC721TransferEvent represents an ERC721 Transfer event type ERC721TransferEvent struct { From common.Address @@ -54,6 +73,15 @@ type ERC721TransferEvent struct { TokenId *big.Int } +// MarshalJSON implements a custom JSON marshaller for the ERC721TransferEvent type +func (e ERC721TransferEvent) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]interface{}{ + "from": e.From.Hex(), + "to": e.To.Hex(), + "tokenId": e.TokenId.String(), + }) +} + // ERC721ApprovalEvent represents an ERC721 Approval event type ERC721ApprovalEvent struct { Owner common.Address @@ -61,6 +89,15 @@ type ERC721ApprovalEvent struct { TokenId *big.Int } +// MarshalJSON implements a custom JSON marshaller for the ERC721ApprovalEvent type +func (e ERC721ApprovalEvent) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]interface{}{ + "owner": e.Owner.Hex(), + "approved": e.Approved.Hex(), + "tokenId": e.TokenId.String(), + }) +} + // ERC721ApprovalForAllEvent represents an ERC721 ApprovalForAll event type ERC721ApprovalForAllEvent struct { Owner common.Address @@ -68,6 +105,15 @@ type ERC721ApprovalForAllEvent struct { Approved bool } +// MarshalJSON implements a custom JSON marshaller for the ERC721ApprovalForAllEvent type +func (e ERC721ApprovalForAllEvent) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]interface{}{ + "owner": e.Owner.Hex(), + "operator": e.Operator.Hex(), + "approved": e.Approved, + }) +} + // ExchangeFillEvent represents a 0x Exchange Fill event type ExchangeFillEvent struct { MakerAddress common.Address @@ -83,6 +129,31 @@ type ExchangeFillEvent struct { TakerAssetData []byte } +// MarshalJSON implements a custom JSON marshaller for the ExchangeFillEvent type +func (e ExchangeFillEvent) MarshalJSON() ([]byte, error) { + makerAssetData := "" + if len(e.MakerAssetData) != 0 { + makerAssetData = fmt.Sprintf("0x%s", common.Bytes2Hex(e.MakerAssetData)) + } + takerAssetData := "" + if len(e.TakerAssetData) != 0 { + takerAssetData = fmt.Sprintf("0x%s", common.Bytes2Hex(e.TakerAssetData)) + } + return json.Marshal(map[string]interface{}{ + "makerAddress": e.MakerAddress.Hex(), + "takerAddress": e.TakerAddress.Hex(), + "senderAddress": e.SenderAddress.Hex(), + "feeRecipientAddress": e.FeeRecipientAddress.Hex(), + "makerAssetFilledAmount": e.MakerAssetFilledAmount.String(), + "takerAssetFilledAmount": e.TakerAssetFilledAmount.String(), + "makerFeePaid": e.MakerFeePaid.String(), + "takerFeePaid": e.TakerFeePaid.String(), + "orderHash": e.OrderHash.Hex(), + "makerAssetData": makerAssetData, + "takerAssetData": takerAssetData, + }) +} + // ExchangeCancelEvent represents a 0x Exchange Cancel event type ExchangeCancelEvent struct { MakerAddress common.Address @@ -93,6 +164,26 @@ type ExchangeCancelEvent struct { TakerAssetData []byte } +// MarshalJSON implements a custom JSON marshaller for the ExchangeCancelEvent type +func (e ExchangeCancelEvent) MarshalJSON() ([]byte, error) { + makerAssetData := "" + if len(e.MakerAssetData) != 0 { + makerAssetData = fmt.Sprintf("0x%s", common.Bytes2Hex(e.MakerAssetData)) + } + takerAssetData := "" + if len(e.TakerAssetData) != 0 { + takerAssetData = fmt.Sprintf("0x%s", common.Bytes2Hex(e.TakerAssetData)) + } + return json.Marshal(map[string]interface{}{ + "makerAddress": e.MakerAddress.Hex(), + "senderAddress": e.SenderAddress.Hex(), + "feeRecipientAddress": e.FeeRecipientAddress.Hex(), + "orderHash": e.OrderHash.Hex(), + "makerAssetData": makerAssetData, + "takerAssetData": takerAssetData, + }) +} + // ExchangeCancelUpToEvent represents a 0x Exchange CancelUpTo event type ExchangeCancelUpToEvent struct { MakerAddress common.Address @@ -100,18 +191,43 @@ type ExchangeCancelUpToEvent struct { OrderEpoch *big.Int } +// MarshalJSON implements a custom JSON marshaller for the ExchangeCancelUpToEvent type +func (e ExchangeCancelUpToEvent) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]interface{}{ + "makerAddress": e.MakerAddress.Hex(), + "senderAddress": e.SenderAddress.Hex(), + "orderEpoch": e.OrderEpoch.String(), + }) +} + // WethWithdrawalEvent represents a wrapped Ether Withdraw event type WethWithdrawalEvent struct { Owner common.Address Value *big.Int } +// MarshalJSON implements a custom JSON marshaller for the WethWithdrawalEvent type +func (w WethWithdrawalEvent) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]interface{}{ + "owner": w.Owner.Hex(), + "value": w.Value.String(), + }) +} + // WethDepositEvent represents a wrapped Ether Deposit event type WethDepositEvent struct { Owner common.Address Value *big.Int } +// MarshalJSON implements a custom JSON marshaller for the WethDepositEvent type +func (w WethDepositEvent) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]interface{}{ + "owner": w.Owner.Hex(), + "value": w.Value.String(), + }) +} + // UnsupportedEventError is thrown when an unsupported topic is encountered type UnsupportedEventError struct { Topics []common.Hash @@ -151,8 +267,8 @@ type Decoder struct { exchangeTopicToEventName map[common.Hash]string } -// NewDecoder instantiates a new 0x order-relevant events decoder -func NewDecoder() (*Decoder, error) { +// New instantiates a new 0x order-relevant events decoder +func New() (*Decoder, error) { erc20ABI, err := abi.JSON(strings.NewReader(erc20EventsAbi)) if err != nil { return nil, err diff --git a/zeroex/orderwatch/decoder/event_decoder_js.go b/zeroex/orderwatch/decoder/event_decoder_js.go new file mode 100644 index 000000000..ed3f343d3 --- /dev/null +++ b/zeroex/orderwatch/decoder/event_decoder_js.go @@ -0,0 +1,116 @@ +// +build js,wasm + +package decoder + +import ( + "fmt" + "syscall/js" + + "github.com/ethereum/go-ethereum/common" +) + +func (e ERC20TransferEvent) JSValue() js.Value { + return js.ValueOf(map[string]interface{}{ + "from": e.From.Hex(), + "to": e.To.Hex(), + "value": e.Value.String(), + }) +} + +func (e ERC20ApprovalEvent) JSValue() js.Value { + return js.ValueOf(map[string]interface{}{ + "owner": e.Owner.Hex(), + "spender": e.Spender.Hex(), + "value": e.Value.String(), + }) +} + +func (e ERC721TransferEvent) JSValue() js.Value { + return js.ValueOf(map[string]interface{}{ + "from": e.From.Hex(), + "to": e.To.Hex(), + "tokenId": e.TokenId.String(), + }) +} + +func (e ERC721ApprovalEvent) JSValue() js.Value { + return js.ValueOf(map[string]interface{}{ + "owner": e.Owner.Hex(), + "approved": e.Approved.Hex(), + "tokenId": e.TokenId.String(), + }) +} + +func (e ERC721ApprovalForAllEvent) JSValue() js.Value { + return js.ValueOf(map[string]interface{}{ + "owner": e.Owner.Hex(), + "operator": e.Operator.Hex(), + "approved": e.Approved, + }) +} + +func (e ExchangeFillEvent) JSValue() js.Value { + makerAssetData := "" + if len(e.MakerAssetData) != 0 { + makerAssetData = fmt.Sprintf("0x%s", common.Bytes2Hex(e.MakerAssetData)) + } + takerAssetData := "" + if len(e.TakerAssetData) != 0 { + takerAssetData = fmt.Sprintf("0x%s", common.Bytes2Hex(e.TakerAssetData)) + } + return js.ValueOf(map[string]interface{}{ + "makerAddress": e.MakerAddress.Hex(), + "takerAddress": e.TakerAddress.Hex(), + "senderAddress": e.SenderAddress.Hex(), + "feeRecipientAddress": e.FeeRecipientAddress.Hex(), + "makerAssetFilledAmount": e.MakerAssetFilledAmount.String(), + "takerAssetFilledAmount": e.TakerAssetFilledAmount.String(), + "makerFeePaid": e.MakerFeePaid.String(), + "takerFeePaid": e.TakerFeePaid.String(), + "orderHash": e.OrderHash.Hex(), + "makerAssetData": makerAssetData, + "takerAssetData": takerAssetData, + }) +} + +func (e ExchangeCancelEvent) JSValue() js.Value { + makerAssetData := "" + if len(e.MakerAssetData) != 0 { + makerAssetData = fmt.Sprintf("0x%s", common.Bytes2Hex(e.MakerAssetData)) + } + takerAssetData := "" + if len(e.TakerAssetData) != 0 { + takerAssetData = fmt.Sprintf("0x%s", common.Bytes2Hex(e.TakerAssetData)) + } + return js.ValueOf(map[string]interface{}{ + "makerAddress": e.MakerAddress.Hex(), + "senderAddress": e.SenderAddress.Hex(), + "feeRecipientAddress": e.FeeRecipientAddress.Hex(), + "orderHash": e.OrderHash.Hex(), + "makerAssetData": makerAssetData, + "takerAssetData": takerAssetData, + }) +} + +func (e ExchangeCancelUpToEvent) JSValue() js.Value { + return js.ValueOf(map[string]interface{}{ + "makerAddress": e.MakerAddress.Hex(), + "senderAddress": e.SenderAddress.Hex(), + "orderEpoch": e.OrderEpoch.String(), + }) +} + +func (w WethWithdrawalEvent) JSValue() js.Value { + return js.ValueOf(map[string]interface{}{ + "owner": w.Owner.Hex(), + "value": w.Value.String(), + }) +} + +func (w WethDepositEvent) JSValue() js.Value { + return js.ValueOf(map[string]interface{}{ + "owner": w.Owner.Hex(), + "value": w.Value.String(), + }) +} + diff --git a/zeroex/orderwatch/event_decoder_test.go b/zeroex/orderwatch/decoder/event_decoder_test.go similarity index 98% rename from zeroex/orderwatch/event_decoder_test.go rename to zeroex/orderwatch/decoder/event_decoder_test.go index 75a1797e4..48ef5a141 100644 --- a/zeroex/orderwatch/event_decoder_test.go +++ b/zeroex/orderwatch/decoder/event_decoder_test.go @@ -1,4 +1,4 @@ -package orderwatch +package decoder import ( "encoding/json" @@ -36,7 +36,7 @@ func TestDecodeERC20Transfer(t *testing.T) { if err != nil { t.Fatal(err.Error()) } - decoder, err := NewDecoder() + decoder, err := New() if err != nil { t.Fatal(err.Error()) } @@ -62,7 +62,7 @@ func TestDecodeERC20Approval(t *testing.T) { if err != nil { t.Fatal(err.Error()) } - decoder, err := NewDecoder() + decoder, err := New() if err != nil { t.Fatal(err.Error()) } @@ -89,7 +89,7 @@ func TestDecodeERC721Transfer(t *testing.T) { if err != nil { t.Fatal(err.Error()) } - decoder, err := NewDecoder() + decoder, err := New() if err != nil { t.Fatal(err.Error()) } @@ -115,7 +115,7 @@ func TestDecodeERC721Approval(t *testing.T) { if err != nil { t.Fatal(err.Error()) } - decoder, err := NewDecoder() + decoder, err := New() if err != nil { t.Fatal(err.Error()) } @@ -141,7 +141,7 @@ func TestDecodeERC721ApprovalForAll(t *testing.T) { if err != nil { t.Fatal(err.Error()) } - decoder, err := NewDecoder() + decoder, err := New() if err != nil { t.Fatal(err.Error()) } @@ -167,7 +167,7 @@ func TestDecodeExchangeFill(t *testing.T) { if err != nil { t.Fatal(err.Error()) } - decoder, err := NewDecoder() + decoder, err := New() if err != nil { t.Fatal(err.Error()) } @@ -200,7 +200,7 @@ func TestDecodeExchangeCancel(t *testing.T) { if err != nil { t.Fatal(err.Error()) } - decoder, err := NewDecoder() + decoder, err := New() if err != nil { t.Fatal(err.Error()) } @@ -227,7 +227,7 @@ func TestDecodeExchangeCancelUpTo(t *testing.T) { if err != nil { t.Fatal(err.Error()) } - decoder, err := NewDecoder() + decoder, err := New() if err != nil { t.Fatal(err.Error()) } @@ -251,7 +251,7 @@ func TestDecodeWethDeposit(t *testing.T) { if err != nil { t.Fatal(err.Error()) } - decoder, err := NewDecoder() + decoder, err := New() if err != nil { t.Fatal(err.Error()) } @@ -274,7 +274,7 @@ func TestDecodeWethWithdrawal(t *testing.T) { if err != nil { t.Fatal(err.Error()) } - decoder, err := NewDecoder() + decoder, err := New() if err != nil { t.Fatal(err.Error()) } diff --git a/zeroex/orderwatch/order_watcher.go b/zeroex/orderwatch/order_watcher.go index 53599e010..4668dce33 100644 --- a/zeroex/orderwatch/order_watcher.go +++ b/zeroex/orderwatch/order_watcher.go @@ -15,8 +15,10 @@ import ( "github.com/0xProject/0x-mesh/meshdb" "github.com/0xProject/0x-mesh/zeroex" "github.com/0xProject/0x-mesh/zeroex/ordervalidator" + "github.com/0xProject/0x-mesh/zeroex/orderwatch/decoder" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/event" + "github.com/ethereum/go-ethereum/rpc" logger "github.com/sirupsen/logrus" ) @@ -42,7 +44,7 @@ var expirationPollingInterval = 50 * time.Millisecond type Watcher struct { meshDB *meshdb.MeshDB blockWatcher *blockwatch.Watcher - eventDecoder *Decoder + eventDecoder *decoder.Decoder assetDataDecoder *zeroex.AssetDataDecoder blockSubscription event.Subscription contractAddresses ethereum.ContractAddresses @@ -58,7 +60,7 @@ type Watcher struct { // New instantiates a new order watcher func New(meshDB *meshdb.MeshDB, blockWatcher *blockwatch.Watcher, orderValidator *ordervalidator.OrderValidator, networkID int, expirationBuffer time.Duration) (*Watcher, error) { - decoder, err := NewDecoder() + decoder, err := decoder.New() if err != nil { return nil, err } @@ -227,7 +229,7 @@ func (w *Watcher) handleExpiration(expiredOrders []expirationwatch.ExpiredItem) OrderHash: common.HexToHash(expiredOrder.ID), SignedOrder: order.SignedOrder, FillableTakerAssetAmount: big.NewInt(0), - Kind: zeroex.EKOrderExpired, + EndState: zeroex.ESOrderExpired, } orderEvents = append(orderEvents, orderEvent) } @@ -239,18 +241,22 @@ func (w *Watcher) handleBlockEvents(events []*blockwatch.Event) error { defer func() { _ = ordersColTxn.Discard() }() - hashToOrderWithTxHashes := map[common.Hash]*OrderWithTxHashes{} + orderHashToDBOrder := map[common.Hash]*meshdb.Order{} + orderHashToEvents := map[common.Hash][]*zeroex.ContractEvent{} + var latestBlockNumber rpc.BlockNumber for _, event := range events { + latestBlockNumber = rpc.BlockNumber(event.BlockHeader.Number.Int64()) for _, log := range event.BlockHeader.Logs { + var parameters zeroex.ContractEventParameters eventType, err := w.eventDecoder.FindEventType(log) if err != nil { switch err.(type) { - case UntrackedTokenError: + case decoder.UntrackedTokenError: continue - case UnsupportedEventError: + case decoder.UnsupportedEventError: logger.WithFields(logger.Fields{ - "topics": err.(UnsupportedEventError).Topics, - "contractAddress": err.(UnsupportedEventError).ContractAddress, + "topics": err.(decoder.UnsupportedEventError).Topics, + "contractAddress": err.(decoder.UnsupportedEventError).ContractAddress, }).Info("unsupported event found while trying to find its event type") continue default: @@ -263,7 +269,7 @@ func (w *Watcher) handleBlockEvents(events []*blockwatch.Event) error { var orders []*meshdb.Order switch eventType { case "ERC20TransferEvent": - var transferEvent ERC20TransferEvent + var transferEvent decoder.ERC20TransferEvent err = w.eventDecoder.Decode(log, &transferEvent) if err != nil { if isNonCritical := w.checkDecodeErr(err, eventType); isNonCritical { @@ -271,6 +277,7 @@ func (w *Watcher) handleBlockEvents(events []*blockwatch.Event) error { } return err } + parameters = transferEvent fromOrders, err := w.findOrders(transferEvent.From, log.Address, nil) if err != nil { return err @@ -283,7 +290,7 @@ func (w *Watcher) handleBlockEvents(events []*blockwatch.Event) error { orders = append(orders, toOrders...) case "ERC20ApprovalEvent": - var approvalEvent ERC20ApprovalEvent + var approvalEvent decoder.ERC20ApprovalEvent err = w.eventDecoder.Decode(log, &approvalEvent) if err != nil { if isNonCritical := w.checkDecodeErr(err, eventType); isNonCritical { @@ -295,13 +302,14 @@ func (w *Watcher) handleBlockEvents(events []*blockwatch.Event) error { if approvalEvent.Spender != w.contractAddresses.ERC20Proxy { continue } + parameters = approvalEvent orders, err = w.findOrders(approvalEvent.Owner, log.Address, nil) if err != nil { return err } case "ERC721TransferEvent": - var transferEvent ERC721TransferEvent + var transferEvent decoder.ERC721TransferEvent err = w.eventDecoder.Decode(log, &transferEvent) if err != nil { if isNonCritical := w.checkDecodeErr(err, eventType); isNonCritical { @@ -309,6 +317,7 @@ func (w *Watcher) handleBlockEvents(events []*blockwatch.Event) error { } return err } + parameters = transferEvent fromOrders, err := w.findOrders(transferEvent.From, log.Address, transferEvent.TokenId) if err != nil { return err @@ -321,7 +330,7 @@ func (w *Watcher) handleBlockEvents(events []*blockwatch.Event) error { orders = append(orders, toOrders...) case "ERC721ApprovalEvent": - var approvalEvent ERC721ApprovalEvent + var approvalEvent decoder.ERC721ApprovalEvent err = w.eventDecoder.Decode(log, &approvalEvent) if err != nil { if isNonCritical := w.checkDecodeErr(err, eventType); isNonCritical { @@ -333,13 +342,14 @@ func (w *Watcher) handleBlockEvents(events []*blockwatch.Event) error { if approvalEvent.Approved != w.contractAddresses.ERC721Proxy { continue } + parameters = approvalEvent orders, err = w.findOrders(approvalEvent.Owner, log.Address, approvalEvent.TokenId) if err != nil { return err } case "ERC721ApprovalForAllEvent": - var approvalForAllEvent ERC721ApprovalForAllEvent + var approvalForAllEvent decoder.ERC721ApprovalForAllEvent err = w.eventDecoder.Decode(log, &approvalForAllEvent) if err != nil { if isNonCritical := w.checkDecodeErr(err, eventType); isNonCritical { @@ -351,13 +361,14 @@ func (w *Watcher) handleBlockEvents(events []*blockwatch.Event) error { if approvalForAllEvent.Operator != w.contractAddresses.ERC721Proxy { continue } + parameters = approvalForAllEvent orders, err = w.findOrders(approvalForAllEvent.Owner, log.Address, nil) if err != nil { return err } case "WethWithdrawalEvent": - var withdrawalEvent WethWithdrawalEvent + var withdrawalEvent decoder.WethWithdrawalEvent err = w.eventDecoder.Decode(log, &withdrawalEvent) if err != nil { if isNonCritical := w.checkDecodeErr(err, eventType); isNonCritical { @@ -365,13 +376,14 @@ func (w *Watcher) handleBlockEvents(events []*blockwatch.Event) error { } return err } + parameters = withdrawalEvent orders, err = w.findOrders(withdrawalEvent.Owner, log.Address, nil) if err != nil { return err } case "WethDepositEvent": - var depositEvent WethDepositEvent + var depositEvent decoder.WethDepositEvent err = w.eventDecoder.Decode(log, &depositEvent) if err != nil { if isNonCritical := w.checkDecodeErr(err, eventType); isNonCritical { @@ -379,13 +391,14 @@ func (w *Watcher) handleBlockEvents(events []*blockwatch.Event) error { } return err } + parameters = depositEvent orders, err = w.findOrders(depositEvent.Owner, log.Address, nil) if err != nil { return err } case "ExchangeFillEvent": - var exchangeFillEvent ExchangeFillEvent + var exchangeFillEvent decoder.ExchangeFillEvent err = w.eventDecoder.Decode(log, &exchangeFillEvent) if err != nil { if isNonCritical := w.checkDecodeErr(err, eventType); isNonCritical { @@ -393,13 +406,14 @@ func (w *Watcher) handleBlockEvents(events []*blockwatch.Event) error { } return err } + parameters = exchangeFillEvent order := w.findOrder(exchangeFillEvent.OrderHash) if order != nil { orders = append(orders, order) } case "ExchangeCancelEvent": - var exchangeCancelEvent ExchangeCancelEvent + var exchangeCancelEvent decoder.ExchangeCancelEvent err = w.eventDecoder.Decode(log, &exchangeCancelEvent) if err != nil { if isNonCritical := w.checkDecodeErr(err, eventType); isNonCritical { @@ -407,6 +421,7 @@ func (w *Watcher) handleBlockEvents(events []*blockwatch.Event) error { } return err } + parameters = exchangeCancelEvent orders = []*meshdb.Order{} order := w.findOrder(exchangeCancelEvent.OrderHash) if order != nil { @@ -414,7 +429,7 @@ func (w *Watcher) handleBlockEvents(events []*blockwatch.Event) error { } case "ExchangeCancelUpToEvent": - var exchangeCancelUpToEvent ExchangeCancelUpToEvent + var exchangeCancelUpToEvent decoder.ExchangeCancelUpToEvent err = w.eventDecoder.Decode(log, &exchangeCancelUpToEvent) if err != nil { if isNonCritical := w.checkDecodeErr(err, eventType); isNonCritical { @@ -422,6 +437,7 @@ func (w *Watcher) handleBlockEvents(events []*blockwatch.Event) error { } return err } + parameters = exchangeCancelUpToEvent orders, err = w.meshDB.FindOrdersByMakerAddressAndMaxSalt(exchangeCancelUpToEvent.MakerAddress, exchangeCancelUpToEvent.OrderEpoch) if err != nil { logger.WithFields(logger.Fields{ @@ -436,22 +452,27 @@ func (w *Watcher) handleBlockEvents(events []*blockwatch.Event) error { }).Error("unknown eventType encountered") return err } + contractEvent := &zeroex.ContractEvent{ + BlockHash: log.BlockHash, + TxHash: log.TxHash, + TxIndex: log.TxIndex, + LogIndex: log.Index, + IsRemoved: log.Removed, + Address: log.Address, + Kind: eventType, + Parameters: parameters, + } for _, order := range orders { - orderWithTxHashes, ok := hashToOrderWithTxHashes[order.Hash] - if !ok { - hashToOrderWithTxHashes[order.Hash] = &OrderWithTxHashes{ - Order: order, - TxHashes: map[common.Hash]interface{}{ - log.TxHash: struct{}{}, - }, - } + orderHashToDBOrder[order.Hash] = order + if _, ok := orderHashToEvents[order.Hash]; !ok { + orderHashToEvents[order.Hash] = []*zeroex.ContractEvent{contractEvent} } else { - orderWithTxHashes.TxHashes[log.TxHash] = struct{}{} + orderHashToEvents[order.Hash] = append(orderHashToEvents[order.Hash], contractEvent) } } } } - return w.generateOrderEventsIfChanged(ordersColTxn, hashToOrderWithTxHashes) + return w.generateOrderEventsIfChanged(ordersColTxn, orderHashToDBOrder, orderHashToEvents, latestBlockNumber) } func (w *Watcher) cleanup(ctx context.Context) error { @@ -468,19 +489,18 @@ func (w *Watcher) cleanup(ctx context.Context) error { }).Error("Failed to find orders by LastUpdatedBefore") return err } - hashToOrderWithTxHashes := map[common.Hash]*OrderWithTxHashes{} + orderHashToDBOrder := map[common.Hash]*meshdb.Order{} + orderHashToEvents := map[common.Hash][]*zeroex.ContractEvent{} // No events when running cleanup job for _, order := range orders { select { case <-ctx.Done(): return nil default: } - hashToOrderWithTxHashes[order.Hash] = &OrderWithTxHashes{ - Order: order, - TxHashes: map[common.Hash]interface{}{}, - } + orderHashToDBOrder[order.Hash] = order + orderHashToEvents[order.Hash] = []*zeroex.ContractEvent{} } - return w.generateOrderEventsIfChanged(ordersColTxn, hashToOrderWithTxHashes) + return w.generateOrderEventsIfChanged(ordersColTxn, orderHashToDBOrder, orderHashToEvents, rpc.LatestBlockNumber) } // Add adds a 0x order to the DB and watches it for changes in fillability. It @@ -512,7 +532,7 @@ func (w *Watcher) Add(orderInfo *ordervalidator.AcceptedOrderInfo) error { OrderHash: orderInfo.OrderHash, SignedOrder: orderInfo.SignedOrder, FillableTakerAssetAmount: orderInfo.FillableTakerAssetAmount, - Kind: zeroex.EKOrderAdded, + EndState: zeroex.ESOrderAdded, } w.orderFeed.Send([]*zeroex.OrderEvent{orderEvent}) @@ -546,11 +566,6 @@ func (w *Watcher) Subscribe(sink chan<- []*zeroex.OrderEvent) event.Subscription return w.orderScope.Track(w.orderFeed.Subscribe(sink)) } -type OrderWithTxHashes struct { - Order *meshdb.Order - TxHashes map[common.Hash]interface{} -} - func (w *Watcher) findOrder(orderHash common.Hash) *meshdb.Order { order := meshdb.Order{} err := w.meshDB.Orders.FindByID(orderHash.Bytes(), &order) @@ -579,10 +594,9 @@ func (w *Watcher) findOrders(makerAddress, tokenAddress common.Address, tokenID return orders, nil } -func (w *Watcher) generateOrderEventsIfChanged(ordersColTxn *db.Transaction, hashToOrderWithTxHashes map[common.Hash]*OrderWithTxHashes) error { +func (w *Watcher) generateOrderEventsIfChanged(ordersColTxn *db.Transaction, orderHashToDBOrder map[common.Hash]*meshdb.Order, orderHashToEvents map[common.Hash][]*zeroex.ContractEvent, validationBlockNumber rpc.BlockNumber) error { signedOrders := []*zeroex.SignedOrder{} - for _, orderWithTxHashes := range hashToOrderWithTxHashes { - order := orderWithTxHashes.Order + for _, order := range orderHashToDBOrder { if order.IsRemoved && time.Since(order.LastUpdated) > permanentlyDeleteAfter { if err := w.permanentlyDeleteOrder(ordersColTxn, order); err != nil { return err @@ -595,18 +609,11 @@ func (w *Watcher) generateOrderEventsIfChanged(ordersColTxn *db.Transaction, has return nil } areNewOrders := false - validationResults := w.orderValidator.BatchValidate(signedOrders, areNewOrders) + validationResults := w.orderValidator.BatchValidate(signedOrders, areNewOrders, validationBlockNumber) orderEvents := []*zeroex.OrderEvent{} for _, acceptedOrderInfo := range validationResults.Accepted { - orderWithTxHashes := hashToOrderWithTxHashes[acceptedOrderInfo.OrderHash] - txHashes := make([]common.Hash, len(orderWithTxHashes.TxHashes)) - txHashIndex := 0 - for txHash := range orderWithTxHashes.TxHashes { - txHashes[txHashIndex] = txHash - txHashIndex++ - } - order := orderWithTxHashes.Order + order := orderHashToDBOrder[acceptedOrderInfo.OrderHash] oldFillableAmount := order.FillableTakerAssetAmount newFillableAmount := acceptedOrderInfo.FillableTakerAssetAmount oldAmountIsMoreThenNewAmount := oldFillableAmount.Cmp(newFillableAmount) == 1 @@ -622,8 +629,8 @@ func (w *Watcher) generateOrderEventsIfChanged(ordersColTxn *db.Transaction, has OrderHash: acceptedOrderInfo.OrderHash, SignedOrder: order.SignedOrder, FillableTakerAssetAmount: acceptedOrderInfo.FillableTakerAssetAmount, - Kind: zeroex.EKOrderAdded, - TxHashes: txHashes, + EndState: zeroex.ESOrderAdded, + ContractEvents: orderHashToEvents[order.Hash], } orderEvents = append(orderEvents, orderEvent) } else if oldFillableAmount.Cmp(newFillableAmount) == 0 { @@ -635,9 +642,9 @@ func (w *Watcher) generateOrderEventsIfChanged(ordersColTxn *db.Transaction, has orderEvent := &zeroex.OrderEvent{ OrderHash: acceptedOrderInfo.OrderHash, SignedOrder: order.SignedOrder, - Kind: zeroex.EKOrderFilled, + EndState: zeroex.ESOrderFilled, FillableTakerAssetAmount: acceptedOrderInfo.FillableTakerAssetAmount, - TxHashes: txHashes, + ContractEvents: orderHashToEvents[order.Hash], } orderEvents = append(orderEvents, orderEvent) } else if oldFillableAmount.Cmp(big.NewInt(0)) == 1 && !oldAmountIsMoreThenNewAmount { @@ -647,9 +654,9 @@ func (w *Watcher) generateOrderEventsIfChanged(ordersColTxn *db.Transaction, has orderEvent := &zeroex.OrderEvent{ OrderHash: acceptedOrderInfo.OrderHash, SignedOrder: order.SignedOrder, - Kind: zeroex.EKOrderFillabilityIncreased, + EndState: zeroex.ESOrderFillabilityIncreased, FillableTakerAssetAmount: acceptedOrderInfo.FillableTakerAssetAmount, - TxHashes: txHashes, + ContractEvents: orderHashToEvents[order.Hash], } orderEvents = append(orderEvents, orderEvent) } @@ -659,8 +666,7 @@ func (w *Watcher) generateOrderEventsIfChanged(ordersColTxn *db.Transaction, has case ordervalidator.MeshError: // TODO(fabio): Do we want to handle MeshErrors somehow here? case ordervalidator.ZeroExValidation: - orderWithTxHashes := hashToOrderWithTxHashes[rejectedOrderInfo.OrderHash] - order := orderWithTxHashes.Order + order := orderHashToDBOrder[rejectedOrderInfo.OrderHash] oldFillableAmount := order.FillableTakerAssetAmount if oldFillableAmount.Cmp(big.NewInt(0)) == 0 { // If the oldFillableAmount was already 0, this order is already flagged for removal. @@ -669,24 +675,18 @@ func (w *Watcher) generateOrderEventsIfChanged(ordersColTxn *db.Transaction, has } else { // If oldFillableAmount > 0, it got fullyFilled, cancelled, expired or unfunded, emit event w.unwatchOrder(ordersColTxn, order, big.NewInt(0)) - kind, ok := ordervalidator.ConvertRejectOrderCodeToOrderEventKind(rejectedOrderInfo.Status) + endState, ok := ordervalidator.ConvertRejectOrderCodeToOrderEventEndState(rejectedOrderInfo.Status) if !ok { - err := fmt.Errorf("no OrderEventKind corresponding to RejectedOrderStatus: %q", rejectedOrderInfo.Status) - logger.WithError(err).WithField("rejectedOrderStatus", rejectedOrderInfo.Status).Error("no OrderEventKind corresponding to RejectedOrderStatus") + err := fmt.Errorf("no OrderEventEndState corresponding to RejectedOrderStatus: %q", rejectedOrderInfo.Status) + logger.WithError(err).WithField("rejectedOrderStatus", rejectedOrderInfo.Status).Error("no OrderEventEndState corresponding to RejectedOrderStatus") return err } - txHashes := make([]common.Hash, len(orderWithTxHashes.TxHashes)) - txHashIndex := 0 - for txHash := range orderWithTxHashes.TxHashes { - txHashes[txHashIndex] = txHash - txHashIndex++ - } orderEvent := &zeroex.OrderEvent{ OrderHash: rejectedOrderInfo.OrderHash, SignedOrder: rejectedOrderInfo.SignedOrder, FillableTakerAssetAmount: big.NewInt(0), - Kind: kind, - TxHashes: txHashes, + EndState: endState, + ContractEvents: orderHashToEvents[order.Hash], } orderEvents = append(orderEvents, orderEvent) } @@ -787,11 +787,11 @@ func (w *Watcher) permanentlyDeleteOrder(ordersColTxn *db.Transaction, order *me // Logs the error and returns true if the error is non-critical. func (w *Watcher) checkDecodeErr(err error, eventType string) bool { - if _, ok := err.(UnsupportedEventError); ok { + if _, ok := err.(decoder.UnsupportedEventError); ok { logger.WithFields(logger.Fields{ "eventType": eventType, - "topics": err.(UnsupportedEventError).Topics, - "contractAddress": err.(UnsupportedEventError).ContractAddress, + "topics": err.(decoder.UnsupportedEventError).Topics, + "contractAddress": err.(decoder.UnsupportedEventError).ContractAddress, }).Warn("unsupported event found") return true } diff --git a/zeroex/orderwatch/order_watcher_test.go b/zeroex/orderwatch/order_watcher_test.go index 9c6d62bc4..6af88d364 100644 --- a/zeroex/orderwatch/order_watcher_test.go +++ b/zeroex/orderwatch/order_watcher_test.go @@ -116,7 +116,7 @@ func TestOrderWatcherUnfundedInsufficientERC20Balance(t *testing.T) { orderEvents := waitForOrderEvents(t, orderEventsChan, 4*time.Second) require.Len(t, orderEvents, 1) orderEvent := orderEvents[0] - assert.Equal(t, zeroex.EKOrderBecameUnfunded, orderEvent.Kind) + assert.Equal(t, zeroex.ESOrderBecameUnfunded, orderEvent.EndState) var orders []*meshdb.Order err = meshDB.Orders.FindAll(&orders) @@ -154,7 +154,7 @@ func TestOrderWatcherUnfundedInsufficientERC721Balance(t *testing.T) { orderEvents := waitForOrderEvents(t, orderEventsChan, 4*time.Second) require.Len(t, orderEvents, 1) orderEvent := orderEvents[0] - assert.Equal(t, zeroex.EKOrderBecameUnfunded, orderEvent.Kind) + assert.Equal(t, zeroex.ESOrderBecameUnfunded, orderEvent.EndState) var orders []*meshdb.Order err = meshDB.Orders.FindAll(&orders) @@ -193,7 +193,7 @@ func TestOrderWatcherUnfundedInsufficientERC721Allowance(t *testing.T) { orderEvents := waitForOrderEvents(t, orderEventsChan, 4*time.Second) require.Len(t, orderEvents, 1) orderEvent := orderEvents[0] - assert.Equal(t, zeroex.EKOrderBecameUnfunded, orderEvent.Kind) + assert.Equal(t, zeroex.ESOrderBecameUnfunded, orderEvent.EndState) var orders []*meshdb.Order err = meshDB.Orders.FindAll(&orders) @@ -232,7 +232,7 @@ func TestOrderWatcherUnfundedInsufficientERC20Allowance(t *testing.T) { orderEvents := waitForOrderEvents(t, orderEventsChan, 4*time.Second) require.Len(t, orderEvents, 1) orderEvent := orderEvents[0] - assert.Equal(t, zeroex.EKOrderBecameUnfunded, orderEvent.Kind) + assert.Equal(t, zeroex.ESOrderBecameUnfunded, orderEvent.EndState) var orders []*meshdb.Order err = meshDB.Orders.FindAll(&orders) @@ -270,7 +270,7 @@ func TestOrderWatcherUnfundedThenFundedAgain(t *testing.T) { orderEvents := waitForOrderEvents(t, orderEventsChan, 4*time.Second) require.Len(t, orderEvents, 1) orderEvent := orderEvents[0] - assert.Equal(t, zeroex.EKOrderBecameUnfunded, orderEvent.Kind) + assert.Equal(t, zeroex.ESOrderBecameUnfunded, orderEvent.EndState) var orders []*meshdb.Order err = meshDB.Orders.FindAll(&orders) @@ -292,7 +292,7 @@ func TestOrderWatcherUnfundedThenFundedAgain(t *testing.T) { orderEvents = <-orderEventsChan require.Len(t, orderEvents, 1) orderEvent = orderEvents[0] - assert.Equal(t, zeroex.EKOrderAdded, orderEvent.Kind) + assert.Equal(t, zeroex.ESOrderAdded, orderEvent.EndState) var newOrders []*meshdb.Order err = meshDB.Orders.FindAll(&newOrders) @@ -375,7 +375,7 @@ func TestOrderWatcherWETHWithdrawAndDeposit(t *testing.T) { orderEvents := waitForOrderEvents(t, orderEventsChan, 4*time.Second) require.Len(t, orderEvents, 1) orderEvent := orderEvents[0] - assert.Equal(t, zeroex.EKOrderBecameUnfunded, orderEvent.Kind) + assert.Equal(t, zeroex.ESOrderBecameUnfunded, orderEvent.EndState) var orders []*meshdb.Order err = meshDB.Orders.FindAll(&orders) @@ -396,7 +396,7 @@ func TestOrderWatcherWETHWithdrawAndDeposit(t *testing.T) { orderEvents = <-orderEventsChan require.Len(t, orderEvents, 1) orderEvent = orderEvents[0] - assert.Equal(t, zeroex.EKOrderAdded, orderEvent.Kind) + assert.Equal(t, zeroex.ESOrderAdded, orderEvent.EndState) var newOrders []*meshdb.Order err = meshDB.Orders.FindAll(&newOrders) @@ -435,7 +435,7 @@ func TestOrderWatcherCanceled(t *testing.T) { orderEvents := waitForOrderEvents(t, orderEventsChan, 4*time.Second) require.Len(t, orderEvents, 1) orderEvent := orderEvents[0] - assert.Equal(t, zeroex.EKOrderCancelled, orderEvent.Kind) + assert.Equal(t, zeroex.ESOrderCancelled, orderEvent.EndState) var orders []*meshdb.Order err = meshDB.Orders.FindAll(&orders) @@ -474,7 +474,7 @@ func TestOrderWatcherCancelUpTo(t *testing.T) { orderEvents := waitForOrderEvents(t, orderEventsChan, 4*time.Second) require.Len(t, orderEvents, 1) orderEvent := orderEvents[0] - assert.Equal(t, zeroex.EKOrderCancelled, orderEvent.Kind) + assert.Equal(t, zeroex.ESOrderCancelled, orderEvent.EndState) var orders []*meshdb.Order err = meshDB.Orders.FindAll(&orders) @@ -513,7 +513,7 @@ func TestOrderWatcherERC20Filled(t *testing.T) { orderEvents := waitForOrderEvents(t, orderEventsChan, 4*time.Second) require.Len(t, orderEvents, 1) orderEvent := orderEvents[0] - assert.Equal(t, zeroex.EKOrderFullyFilled, orderEvent.Kind) + assert.Equal(t, zeroex.ESOrderFullyFilled, orderEvent.EndState) var orders []*meshdb.Order err = meshDB.Orders.FindAll(&orders) diff --git a/zeroex/orderwatch/topics.go b/zeroex/orderwatch/topics.go index d9f940f4b..2f86efc31 100644 --- a/zeroex/orderwatch/topics.go +++ b/zeroex/orderwatch/topics.go @@ -1,6 +1,7 @@ package orderwatch import ( + "github.com/0xProject/0x-mesh/zeroex/orderwatch/decoder" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/crypto" ) @@ -9,7 +10,7 @@ import ( // the logs retrieved for Ethereum blocks func GetRelevantTopics() []common.Hash { topics := []common.Hash{} - for _, signature := range EVENT_SIGNATURES { + for _, signature := range decoder.EVENT_SIGNATURES { topic := common.BytesToHash(crypto.Keccak256([]byte(signature))) topics = append(topics, topic) }