diff --git a/mocks/polygon/graph_ql.go b/mocks/polygon/graph_ql.go index 6e29d97..557f68e 100644 --- a/mocks/polygon/graph_ql.go +++ b/mocks/polygon/graph_ql.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.9.4. DO NOT EDIT. +// Code generated by mockery v0.0.0-dev. DO NOT EDIT. package polygon diff --git a/mocks/polygon/jsonrpc.go b/mocks/polygon/jsonrpc.go index 63a499f..c0e4032 100644 --- a/mocks/polygon/jsonrpc.go +++ b/mocks/polygon/jsonrpc.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.9.4. DO NOT EDIT. +// Code generated by mockery v0.0.0-dev. DO NOT EDIT. package polygon diff --git a/mocks/services/client.go b/mocks/services/client.go index 98dffbc..1c35920 100644 --- a/mocks/services/client.go +++ b/mocks/services/client.go @@ -1,4 +1,4 @@ -// Code generated by mockery 2.9.4. DO NOT EDIT. +// Code generated by mockery v0.0.0-dev. DO NOT EDIT. package services @@ -69,6 +69,29 @@ func (_m *Client) Block(_a0 context.Context, _a1 *types.PartialBlockIdentifier) return r0, r1 } +// BlockHeader provides a mock function with given fields: ctx, number +func (_m *Client) BlockHeader(ctx context.Context, number *big.Int) (*coretypes.Header, error) { + ret := _m.Called(ctx, number) + + var r0 *coretypes.Header + if rf, ok := ret.Get(0).(func(context.Context, *big.Int) *coretypes.Header); ok { + r0 = rf(ctx, number) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*coretypes.Header) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context, *big.Int) error); ok { + r1 = rf(ctx, number) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // Call provides a mock function with given fields: ctx, request func (_m *Client) Call(ctx context.Context, request *types.CallRequest) (*types.CallResponse, error) { ret := _m.Called(ctx, request) @@ -196,13 +219,13 @@ func (_m *Client) Status(_a0 context.Context) (*types.BlockIdentifier, int64, *t return r0, r1, r2, r3, r4 } -// SuggestGasPrice provides a mock function with given fields: ctx, gasPrice -func (_m *Client) SuggestGasPrice(ctx context.Context, gasPrice *big.Int) (*big.Int, error) { - ret := _m.Called(ctx, gasPrice) +// SuggestGasTipCap provides a mock function with given fields: ctx +func (_m *Client) SuggestGasTipCap(ctx context.Context) (*big.Int, error) { + ret := _m.Called(ctx) var r0 *big.Int - if rf, ok := ret.Get(0).(func(context.Context, *big.Int) *big.Int); ok { - r0 = rf(ctx, gasPrice) + if rf, ok := ret.Get(0).(func(context.Context) *big.Int); ok { + r0 = rf(ctx) } else { if ret.Get(0) != nil { r0 = ret.Get(0).(*big.Int) @@ -210,8 +233,8 @@ func (_m *Client) SuggestGasPrice(ctx context.Context, gasPrice *big.Int) (*big. } var r1 error - if rf, ok := ret.Get(1).(func(context.Context, *big.Int) error); ok { - r1 = rf(ctx, gasPrice) + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) } else { r1 = ret.Error(1) } diff --git a/polygon/client.go b/polygon/client.go index 13eebef..7f350d1 100644 --- a/polygon/client.go +++ b/polygon/client.go @@ -87,6 +87,7 @@ type Client struct { burntContract map[string]string } +// ClientConfig holds asset config information type ClientConfig struct { URL string ChainConfig *params.ChainConfig @@ -149,7 +150,7 @@ func (ec *Client) Status(ctx context.Context) ( []*RosettaTypes.Peer, error, ) { - header, err := ec.blockHeader(ctx, nil) + header, err := ec.BlockHeader(ctx, nil) if err != nil { return nil, -1, nil, nil, err } @@ -193,15 +194,11 @@ func (ec *Client) PendingNonceAt(ctx context.Context, account common.Address) (u return uint64(result), err } -// SuggestGasPrice retrieves the currently suggested gas price to allow a timely -// execution of a transaction. -func (ec *Client) SuggestGasPrice(ctx context.Context, gasPrice *big.Int) (*big.Int, error) { - if gasPrice != nil { - return gasPrice, nil - } - +// SuggestGasTipCap retrieves the currently suggested gas tip cap after 1559 to +// allow a timely execution of a transaction. +func (ec *Client) SuggestGasTipCap(ctx context.Context) (*big.Int, error) { var hex hexutil.Big - if err := ec.c.CallContext(ctx, &hex, "eth_gasPrice"); err != nil { + if err := ec.c.CallContext(ctx, &hex, "eth_maxPriorityFeePerGas"); err != nil { return nil, err } return (*big.Int)(&hex), nil @@ -255,7 +252,9 @@ func (ec *Client) SendTransaction(ctx context.Context, tx *types.Transaction) er if err != nil { return err } - return ec.c.CallContext(ctx, nil, "eth_sendRawTransaction", hexutil.Encode(data)) + // We have to remove the first two bytes otherwise DynamicFeeTxs will not send: + // https://ethereum.stackexchange.com/questions/124447/eth-sendrawtransaction-with-dynamicfeetx-returns-expected-input-list-for-types + return ec.c.CallContext(ctx, nil, "eth_sendRawTransaction", hexutil.Encode(data[2:])) } func toBlockNumArg(number *big.Int) string { @@ -289,9 +288,9 @@ func (ec *Client) Block( return ec.getParsedBlock(ctx, "eth_getBlockByNumber", toBlockNumArg(nil), true) } -// Header returns a block header from the current canonical chain. If number is +// BlockHeader returns a block header from the current canonical chain. If number is // nil, the latest known header is returned. -func (ec *Client) blockHeader(ctx context.Context, number *big.Int) (*types.Header, error) { +func (ec *Client) BlockHeader(ctx context.Context, number *big.Int) (*types.Header, error) { var head *types.Header err := ec.c.CallContext(ctx, &head, "eth_getBlockByNumber", toBlockNumArg(number), false) if err == nil && head == nil { @@ -1517,7 +1516,7 @@ func toCallArg(msg ethereum.CallMsg) interface{} { return arg } -// This implementation is taken from: +// CalculateBurntContract implementation is taken from: // https://github.com/maticnetwork/bor/blob/c227a072418626dd758ceabffd2ea7dadac6eecb/params/config.go#L527 // // TODO: Depend on maticnetwork fork of go-ethereum instead of stock geth so we don't need to diff --git a/polygon/client_test.go b/polygon/client_test.go index 85006be..ae0caf7 100644 --- a/polygon/client_test.go +++ b/polygon/client_test.go @@ -1360,73 +1360,6 @@ func TestPendingNonceAt(t *testing.T) { mockGraphQL.AssertExpectations(t) } -func TestSuggestGasPrice_Nil(t *testing.T) { - mockJSONRPC := &mocks.JSONRPC{} - mockGraphQL := &mocks.GraphQL{} - - cf, err := newERC20CurrencyFetcher(mockGraphQL) - assert.NoError(t, err) - - c := &Client{ - c: mockJSONRPC, - g: mockGraphQL, - currencyFetcher: cf, - traceSemaphore: semaphore.NewWeighted(100), - } - - ctx := context.Background() - mockJSONRPC.On( - "CallContext", - ctx, - mock.Anything, - "eth_gasPrice", - ).Return( - nil, - ).Run( - func(args mock.Arguments) { - r := args.Get(1).(*hexutil.Big) - - *r = *(*hexutil.Big)(big.NewInt(100000)) - }, - ).Once() - resp, err := c.SuggestGasPrice( - ctx, - nil, - ) - assert.Equal(t, big.NewInt(100000), resp) - assert.NoError(t, err) - - mockJSONRPC.AssertExpectations(t) - mockGraphQL.AssertExpectations(t) -} - -func TestSuggestGasPrice_Valid(t *testing.T) { - mockJSONRPC := &mocks.JSONRPC{} - mockGraphQL := &mocks.GraphQL{} - - cf, err := newERC20CurrencyFetcher(mockGraphQL) - assert.NoError(t, err) - - c := &Client{ - c: mockJSONRPC, - g: mockGraphQL, - currencyFetcher: cf, - traceSemaphore: semaphore.NewWeighted(100), - } - - ctx := context.Background() - gasPrice, _ := new(big.Int).SetString("100000000000", 10) - resp, err := c.SuggestGasPrice( - ctx, - gasPrice, - ) - assert.Equal(t, gasPrice, resp) - assert.NoError(t, err) - - mockJSONRPC.AssertExpectations(t) - mockGraphQL.AssertExpectations(t) -} - func TestSendTransaction(t *testing.T) { mockJSONRPC := &mocks.JSONRPC{} mockGraphQL := &mocks.GraphQL{} @@ -1447,7 +1380,7 @@ func TestSendTransaction(t *testing.T) { ctx, mock.Anything, "eth_sendRawTransaction", - "0xf86a80843b9aca00825208941ff502f9fe838cd772874cb67d0d96b93fd1d6d78725d4b6199a415d8029a01d110bf9fd468f7d00b3ce530832e99818835f45e9b08c66f8d9722264bb36c7a02711f47ec99f9ac585840daef41b7118b52ec72f02fcb30d874d36b10b668b59", // nolint + "0x80843b9aca00825208941ff502f9fe838cd772874cb67d0d96b93fd1d6d78725d4b6199a415d8029a01d110bf9fd468f7d00b3ce530832e99818835f45e9b08c66f8d9722264bb36c7a02711f47ec99f9ac585840daef41b7118b52ec72f02fcb30d874d36b10b668b59", // nolint ).Return( nil, ).Once() diff --git a/services/construction/combine.go b/services/construction/combine.go index c15602f..6b52ce8 100644 --- a/services/construction/combine.go +++ b/services/construction/combine.go @@ -43,16 +43,18 @@ func (a *APIService) ConstructionCombine( return nil, svcErrors.WrapErr(svcErrors.ErrUnableToParseIntermediateResult, err) } - ethTransaction := ethTypes.NewTransaction( - unsignedTx.Nonce, - common.HexToAddress(unsignedTx.To), - unsignedTx.Value, - unsignedTx.GasLimit, - unsignedTx.GasPrice, - unsignedTx.Data, - ) - - signer := ethTypes.NewEIP155Signer(unsignedTx.ChainID) + toAddress := common.HexToAddress(unsignedTx.To) + ethTransaction := ethTypes.NewTx( + ðTypes.DynamicFeeTx{ + Nonce: unsignedTx.Nonce, + To: &toAddress, + Value: unsignedTx.Value, + Gas: unsignedTx.GasLimit, + GasFeeCap: unsignedTx.GasCap, + GasTipCap: unsignedTx.GasTip, + Data: unsignedTx.Data, + }) + signer := ethTypes.NewLondonSigner(unsignedTx.ChainID) signedTx, err := ethTransaction.WithSignature(signer, request.Signatures[0].Bytes) if err != nil { return nil, svcErrors.WrapErr(svcErrors.ErrInvalidSignature, err) diff --git a/services/construction/combine_test.go b/services/construction/combine_test.go index 4d6bbb1..eef9059 100644 --- a/services/construction/combine_test.go +++ b/services/construction/combine_test.go @@ -22,12 +22,13 @@ import ( "github.com/coinbase/rosetta-sdk-go/types" svcError "github.com/maticnetwork/polygon-rosetta/services/errors" "github.com/stretchr/testify/assert" -) + ) + func TestConstructionCombine(t *testing.T) { - unsignedRaw := `{"from":"0xD10a72Cf054650931365Cc44D912a4FD75257058","to":"0x57B414a0332B5CaB885a451c2a28a07d1e9b8a8d","value":"0x9864aac3510d02","data":"0x","nonce":"0x0","gas_price":"0x3b9aca00","gas":"0x5208","chain_id":"0x13881"}` // nolint - signedRaw := `{"type":"0x0","nonce":"0x0","gasPrice":"0x3b9aca00","maxPriorityFeePerGas":null,"maxFeePerGas":null,"gas":"0x5208","value":"0x9864aac3510d02","input":"0x","v":"0x27126","r":"0x303b2ff05024c20f1775dad9a6e8152fa75bec47c051d7fd2e39572fbddd048e","s":"0xf2c494280dfa0465d384280dc918c930aae0874714e893382c16058aadf505","to":"0x57b414a0332b5cab885a451c2a28a07d1e9b8a8d","hash":"0x2500ef3f8531452210cfdfe3c11111e9605a2acdd260ac75c8c3ade30258228e"}` // nolint - signaturesRaw := `[{"hex_bytes":"303b2ff05024c20f1775dad9a6e8152fa75bec47c051d7fd2e39572fbddd048e00f2c494280dfa0465d384280dc918c930aae0874714e893382c16058aadf50501","signing_payload":{"address":"0xD10a72Cf054650931365Cc44D912a4FD75257058","hex_bytes":"375623b2f9164db0bc050c357fb4e6b57a60ffa1eba0161fe12e96384103218c","account_identifier":{"address":"0xD10a72Cf054650931365Cc44D912a4FD75257058"},"signature_type":"ecdsa_recovery"},"public_key":{"hex_bytes":"0212e9f98d9750e5f74b4b4b00df39074f86c79187943bdb3c5a9c89ffc1ed0188","curve_type":"secp256k1"},"signature_type":"ecdsa_recovery"}]` // nolint + unsignedRaw := `{"from":"0x5aCB42b3cfCD734a57AFF800139ba1354b549159","to":"0x3Fa177c2E87Cb24148EC403921dB577d140CC07c","value":"0x3e8","data":"0x","nonce":"0x2","gas_price":"0x0","max_fee_per_gas":"0x59682f15","max_priority_fee_per_gas":"0x59682eff","gas":"0x5208","chain_id":"0x13881"}` // nolint + signedRaw := `{"type":"0x2","nonce":"0x2","gasPrice":null,"maxPriorityFeePerGas":"0x59682eff","maxFeePerGas":"0x59682f15","gas":"0x5208","value":"0x3e8","input":"0x","v":"0x1","r":"0x6afe2f65d311ff2430ca7388335b86e42606ea4728924d91564405df83d2cea5","s":"0x443a04f2d96ea9877ed67f2b45266446ab01de2154c268470f57bb12effa1563","to":"0x3fa177c2e87cb24148ec403921db577d140cc07c","chainId":"0x13881","accessList":[],"hash":"0x554c2edbd04b2be9d1314ef31201e3382eedb24a733f1b15448af2d16252db73"}` // nolint + signaturesRaw := `[{"hex_bytes": "6afe2f65d311ff2430ca7388335b86e42606ea4728924d91564405df83d2cea5443a04f2d96ea9877ed67f2b45266446ab01de2154c268470f57bb12effa156301", "public_key": {"hex_bytes": "0405e82ac561143aafc13ba109677a597c8f797b07417d0addd7a346ad35882b3c4a006620e02127b9a32e90979ff93ecad0a2f577db238163a50023e393e354ff", "curve_type": "secp256k1"}, "signing_payload": {"hex_bytes": "15ff43e2bc6aacc7d0f0ed76eb3102aaf9b1292e2ba07575a4e4f3ddb5b54780", "address": "0x5aCB42b3cfCD734a57AFF800139ba1354b549159"}, "signature_type": "ecdsa_recovery"}]` // nolint var signatures []*types.Signature _ = json.Unmarshal([]byte(signaturesRaw), &signatures) diff --git a/services/construction/construction_service_test.go b/services/construction/construction_service_test.go index 88deb4e..a22caac 100644 --- a/services/construction/construction_service_test.go +++ b/services/construction/construction_service_test.go @@ -18,6 +18,7 @@ import ( "context" "encoding/hex" "encoding/json" + EthTypes "github.com/ethereum/go-ethereum/core/types" "math/big" "testing" @@ -48,19 +49,42 @@ var ( toAddress = "0xefD3dc58D60aF3295B92ecd484CAEB3A2f30b3e7" tokenContractAddress = "0x2d7882beDcbfDDce29Ba99965dd3cdF7fcB10A1e" + constructionFromAddress = "0x5aCB42b3cfCD734a57AFF800139ba1354b549159" + constructionToAddress = "0x3Fa177c2E87Cb24148EC403921dB577d140CC07c" + transferValue = uint64(20211004) - transferGasPrice = uint64(5000000000) transferGasLimit = uint64(21000) transferGasLimitERC20 = uint64(65000) transferNonce = uint64(67) transferData = "0xa9059cbb000000000000000000000000efd3dc58d60af3295b92ecd484caeb3a2f30b3e7000000000000000000000000000000000000000000000000000000000134653c" //nolint + transferGasCap = uint64(1001000000) + transferGasTip = uint64(1000000000) transferValueHex = hexutil.EncodeUint64(transferValue) - transferGasPriceHex = hexutil.EncodeUint64(transferGasPrice) transferGasLimitHex = hexutil.EncodeUint64(transferGasLimit) transferGasLimitERC20Hex = hexutil.EncodeUint64(transferGasLimitERC20) transferNonceHex = hexutil.EncodeUint64(transferNonce) transferNonceHex2 = "0x22" + transferGasCapHex = hexutil.EncodeUint64(transferGasCap) + transferGasTipHex = hexutil.EncodeUint64(transferGasTip) + + + header = EthTypes.Header{ + ParentHash: common.Hash{}, + UncleHash: common.Hash{}, + Coinbase: common.Address{}, + Root: common.Hash{}, + TxHash: common.Hash{}, + ReceiptHash: common.Hash{}, + Bloom: EthTypes.Bloom{}, + Difficulty: nil, + Number: nil, + GasLimit: 0, + GasUsed: 0, + Time: 0, + Extra: hexutil.Bytes{}, + BaseFee: big.NewInt(500000), + } ) func forceHexDecode(t *testing.T, s string) []byte { @@ -113,7 +137,7 @@ func TestConstructionFlowWithPendingNonce(t *testing.T) { }, deriveResponse) // Test Preprocess - intent := `[{"operation_identifier":{"index":0},"type":"CALL","account":{"address":"0xD10a72Cf054650931365Cc44D912a4FD75257058"},"amount":{"value":"-42894881044106498","currency":{"symbol":"MATIC","decimals":18}}},{"operation_identifier":{"index":1},"type":"CALL","account":{"address":"0x57B414a0332B5CaB885a451c2a28a07d1e9b8a8d"},"amount":{"value":"42894881044106498","currency":{"symbol":"MATIC","decimals":18}}}]` // nolint + intent := `[{"operation_identifier":{"index":0},"type":"CALL","account":{"address":"0x5aCB42b3cfCD734a57AFF800139ba1354b549159"},"amount":{"value":"-1000","currency":{"symbol":"MATIC","decimals":18}}},{"operation_identifier":{"index":1},"related_operations":[{"index":0}],"type":"CALL","account":{"address":"0x3Fa177c2E87Cb24148EC403921dB577d140CC07c"},"amount":{"value":"1000","currency":{"symbol":"MATIC","decimals":18}}}]` var ops []*types.Operation assert.NoError(t, json.Unmarshal([]byte(intent), &ops)) preprocessResponse, err := servicer.ConstructionPreprocess( @@ -124,7 +148,7 @@ func TestConstructionFlowWithPendingNonce(t *testing.T) { }, ) assert.Nil(t, err) - optionsRaw := `{"from":"0xD10a72Cf054650931365Cc44D912a4FD75257058", "to": "0x57B414a0332B5CaB885a451c2a28a07d1e9b8a8d", "value":"0x9864aac3510d02"}` //nolint + optionsRaw := `{"from":"0x5aCB42b3cfCD734a57AFF800139ba1354b549159","to":"0x3Fa177c2E87Cb24148EC403921dB577d140CC07c","value":"0x3e8"}` var options options assert.NoError(t, json.Unmarshal([]byte(optionsRaw), &options)) assert.Equal(t, &types.ConstructionPreprocessResponse{ @@ -134,26 +158,34 @@ func TestConstructionFlowWithPendingNonce(t *testing.T) { // Test Metadata metadata := &metadata{ GasLimit: 21000, - GasPrice: big.NewInt(1000000000), + GasTip: big.NewInt(1500000000), + GasCap: big.NewInt(1501000000), Nonce: 0, - To: "0x57B414a0332B5CaB885a451c2a28a07d1e9b8a8d", - Value: big.NewInt(42894881044106498), + To: constructionToAddress, + Value: big.NewInt(1000), } - var gasPrice *big.Int = nil + var blockNum *big.Int = nil mockClient.On( - "SuggestGasPrice", + "BlockHeader", + ctx, + blockNum, + ).Return( + &header, + nil, + ).Once() + mockClient.On( + "SuggestGasTipCap", ctx, - gasPrice, ).Return( - big.NewInt(1000000000), + big.NewInt(1500000000), nil, ).Once() mockClient.On( "PendingNonceAt", ctx, - common.HexToAddress("0xD10a72Cf054650931365Cc44D912a4FD75257058"), + common.HexToAddress(constructionFromAddress), ).Return( uint64(0), nil, @@ -167,21 +199,21 @@ func TestConstructionFlowWithPendingNonce(t *testing.T) { Metadata: forceMarshalMap(t, metadata), SuggestedFee: []*types.Amount{ { - Value: "21000000000000", + Value: "31510500000000", Currency: polygon.Currency, }, }, }, metadataResponse) // Test Payloads - unsignedRaw := `{"from":"0xD10a72Cf054650931365Cc44D912a4FD75257058","to":"0x57B414a0332B5CaB885a451c2a28a07d1e9b8a8d","value":"0x9864aac3510d02","data":"0x","nonce":"0x0","gas_price":"0x3b9aca00","gas":"0x5208","chain_id":"0x13881"}` // nolint + unsignedRaw := `{"from":"0x5aCB42b3cfCD734a57AFF800139ba1354b549159","to":"0x3Fa177c2E87Cb24148EC403921dB577d140CC07c","value":"0x3e8","data":"0x","nonce":"0x0","max_fee_per_gas":"0x59777140","max_priority_fee_per_gas":"0x59682f00","gas":"0x5208","chain_id":"0x13881"}` payloadsResponse, err := servicer.ConstructionPayloads(ctx, &types.ConstructionPayloadsRequest{ NetworkIdentifier: networkIdentifier, Operations: ops, Metadata: forceMarshalMap(t, metadata), }) assert.Nil(t, err) - payloadsRaw := `[{"address":"0xD10a72Cf054650931365Cc44D912a4FD75257058","hex_bytes":"375623b2f9164db0bc050c357fb4e6b57a60ffa1eba0161fe12e96384103218c","account_identifier":{"address":"0xD10a72Cf054650931365Cc44D912a4FD75257058"},"signature_type":"ecdsa_recovery"}]` // nolint + payloadsRaw := `[{"address":"0x5aCB42b3cfCD734a57AFF800139ba1354b549159","hex_bytes":"9df2732e3102b2de6c837eb1055292b1f0472b6ae898dff7ba917afc61719120","account_identifier":{"address":"0x5aCB42b3cfCD734a57AFF800139ba1354b549159"},"signature_type":"ecdsa_recovery"}]` var payloads []*types.SigningPayload assert.NoError(t, json.Unmarshal([]byte(payloadsRaw), &payloads)) assert.Equal(t, &types.ConstructionPayloadsResponse{ @@ -190,7 +222,7 @@ func TestConstructionFlowWithPendingNonce(t *testing.T) { }, payloadsResponse) // Test Parse Unsigned - parseOpsRaw := `[{"operation_identifier":{"index":0},"type":"CALL","account":{"address":"0xD10a72Cf054650931365Cc44D912a4FD75257058"},"amount":{"value":"-42894881044106498","currency":{"symbol":"MATIC","decimals":18}}},{"operation_identifier":{"index":1},"related_operations":[{"index":0}],"type":"CALL","account":{"address":"0x57B414a0332B5CaB885a451c2a28a07d1e9b8a8d"},"amount":{"value":"42894881044106498","currency":{"symbol":"MATIC","decimals":18}}}]` // nolint + parseOpsRaw := `[{"operation_identifier":{"index":0},"type":"CALL","account":{"address":"0x5aCB42b3cfCD734a57AFF800139ba1354b549159"},"amount":{"value":"-1000","currency":{"symbol":"MATIC","decimals":18}}},{"operation_identifier":{"index":1},"related_operations":[{"index":0}],"type":"CALL","account":{"address":"0x3Fa177c2E87Cb24148EC403921dB577d140CC07c"},"amount":{"value":"1000","currency":{"symbol":"MATIC","decimals":18}}}]` var parseOps []*types.Operation assert.NoError(t, json.Unmarshal([]byte(parseOpsRaw), &parseOps)) parseUnsignedResponse, err := servicer.ConstructionParse(ctx, &types.ConstructionParseRequest{ @@ -201,7 +233,8 @@ func TestConstructionFlowWithPendingNonce(t *testing.T) { assert.Nil(t, err) parseMetadata := &parseMetadata{ Nonce: metadata.Nonce, - GasPrice: metadata.GasPrice, + GasCap: metadata.GasCap, + GasTip: metadata.GasTip, GasLimit: metadata.GasLimit, ChainID: big.NewInt(80001), } @@ -212,10 +245,10 @@ func TestConstructionFlowWithPendingNonce(t *testing.T) { }, parseUnsignedResponse) // Test Combine - signaturesRaw := `[{"hex_bytes":"303b2ff05024c20f1775dad9a6e8152fa75bec47c051d7fd2e39572fbddd048e00f2c494280dfa0465d384280dc918c930aae0874714e893382c16058aadf50501","signing_payload":{"address":"0xD10a72Cf054650931365Cc44D912a4FD75257058","hex_bytes":"375623b2f9164db0bc050c357fb4e6b57a60ffa1eba0161fe12e96384103218c","account_identifier":{"address":"0xD10a72Cf054650931365Cc44D912a4FD75257058"},"signature_type":"ecdsa_recovery"},"public_key":{"hex_bytes":"0212e9f98d9750e5f74b4b4b00df39074f86c79187943bdb3c5a9c89ffc1ed0188","curve_type":"secp256k1"},"signature_type":"ecdsa_recovery"}]` // nolint + signaturesRaw := `[{"hex_bytes":"9f2f61a9a90f6695b10ed04102dca3e0aa50a10263afd861761226e3e61903a62fb42a9e8d96af626e64fd20573338f41d4f90ea9834ce6aa4ff79869181699700","public_key":{"hex_bytes":"0405e82ac561143aafc13ba109677a597c8f797b07417d0addd7a346ad35882b3c4a006620e02127b9a32e90979ff93ecad0a2f577db238163a50023e393e354ff","curve_type":"secp256k1"},"signing_payload":{"hex_bytes":"9df2732e3102b2de6c837eb1055292b1f0472b6ae898dff7ba917afc61719120","address":"0x5aCB42b3cfCD734a57AFF800139ba1354b549159"},"signature_type":"ecdsa_recovery"}]` var signatures []*types.Signature assert.NoError(t, json.Unmarshal([]byte(signaturesRaw), &signatures)) - signedRaw := `{"type":"0x0","nonce":"0x0","gasPrice":"0x3b9aca00","maxPriorityFeePerGas":null,"maxFeePerGas":null,"gas":"0x5208","value":"0x9864aac3510d02","input":"0x","v":"0x27126","r":"0x303b2ff05024c20f1775dad9a6e8152fa75bec47c051d7fd2e39572fbddd048e","s":"0xf2c494280dfa0465d384280dc918c930aae0874714e893382c16058aadf505","to":"0x57b414a0332b5cab885a451c2a28a07d1e9b8a8d","hash":"0x2500ef3f8531452210cfdfe3c11111e9605a2acdd260ac75c8c3ade30258228e"}` // nolint + signedRaw := `{"type":"0x2","nonce":"0x0","gasPrice":null,"maxPriorityFeePerGas":"0x59682f00","maxFeePerGas":"0x59777140","gas":"0x5208","value":"0x3e8","input":"0x","v":"0x0","r":"0x9f2f61a9a90f6695b10ed04102dca3e0aa50a10263afd861761226e3e61903a6","s":"0x2fb42a9e8d96af626e64fd20573338f41d4f90ea9834ce6aa4ff798691816997","to":"0x3fa177c2e87cb24148ec403921db577d140cc07c","chainId":"0x13881","accessList":[],"hash":"0xfacf81ceb293b34292ea428c64a4a550fd5702432908a952aa9d1db455c22c72"}` //nolint combineResponse, err := servicer.ConstructionCombine(ctx, &types.ConstructionCombineRequest{ NetworkIdentifier: networkIdentifier, UnsignedTransaction: unsignedRaw, @@ -227,6 +260,9 @@ func TestConstructionFlowWithPendingNonce(t *testing.T) { }, combineResponse) // Test Parse Signed + var parseSignedOps []*types.Operation + assert.NoError(t, json.Unmarshal([]byte(parseOpsRaw), &parseSignedOps)) + parseSignedResponse, err := servicer.ConstructionParse(ctx, &types.ConstructionParseRequest{ NetworkIdentifier: networkIdentifier, Signed: true, @@ -234,16 +270,16 @@ func TestConstructionFlowWithPendingNonce(t *testing.T) { }) assert.Nil(t, err) assert.Equal(t, &types.ConstructionParseResponse{ - Operations: parseOps, + Operations: parseSignedOps, AccountIdentifierSigners: []*types.AccountIdentifier{ - {Address: "0xD10a72Cf054650931365Cc44D912a4FD75257058"}, + {Address: constructionFromAddress}, }, Metadata: forceMarshalMap(t, parseMetadata), }, parseSignedResponse) // Test Hash transactionIdentifier := &types.TransactionIdentifier{ - Hash: "0x2500ef3f8531452210cfdfe3c11111e9605a2acdd260ac75c8c3ade30258228e", + Hash: "0xfacf81ceb293b34292ea428c64a4a550fd5702432908a952aa9d1db455c22c72", } hashResponse, err := servicer.ConstructionHash(ctx, &types.ConstructionHashRequest{ NetworkIdentifier: networkIdentifier, @@ -311,7 +347,7 @@ func TestConstructionFlowWithInputNonce(t *testing.T) { }, deriveResponse) // Test Preprocess - intent := `[{"operation_identifier":{"index":0},"type":"CALL","account":{"address":"0xD10a72Cf054650931365Cc44D912a4FD75257058"},"amount":{"value":"-42894881044106498","currency":{"symbol":"MATIC","decimals":18}}},{"operation_identifier":{"index":1},"type":"CALL","account":{"address":"0x57B414a0332B5CaB885a451c2a28a07d1e9b8a8d"},"amount":{"value":"42894881044106498","currency":{"symbol":"MATIC","decimals":18}}}]` // nolint + intent := `[{"operation_identifier":{"index":0},"type":"CALL","account":{"address":"0x5aCB42b3cfCD734a57AFF800139ba1354b549159"},"amount":{"value":"-1000","currency":{"symbol":"MATIC","decimals":18}}},{"operation_identifier":{"index":1},"related_operations":[{"index":0}],"type":"CALL","account":{"address":"0x3Fa177c2E87Cb24148EC403921dB577d140CC07c"},"amount":{"value":"1000","currency":{"symbol":"MATIC","decimals":18}}}]` var ops []*types.Operation assert.NoError(t, json.Unmarshal([]byte(intent), &ops)) preprocessResponse, err := servicer.ConstructionPreprocess( @@ -323,7 +359,7 @@ func TestConstructionFlowWithInputNonce(t *testing.T) { }, ) assert.Nil(t, err) - optionsRaw := `{"from":"0xD10a72Cf054650931365Cc44D912a4FD75257058", "nonce":"0x1", "to":"0x57B414a0332B5CaB885a451c2a28a07d1e9b8a8d", "value":"0x9864aac3510d02"}` // nolint + optionsRaw := `{"from":"0x5aCB42b3cfCD734a57AFF800139ba1354b549159","nonce":"0x1","to":"0x3Fa177c2E87Cb24148EC403921dB577d140CC07c","value":"0x3e8"}` var options options assert.NoError(t, json.Unmarshal([]byte(optionsRaw), &options)) assert.Equal(t, &types.ConstructionPreprocessResponse{ @@ -333,20 +369,28 @@ func TestConstructionFlowWithInputNonce(t *testing.T) { // Test Metadata metadata := &metadata{ GasLimit: 21000, - GasPrice: big.NewInt(1000000000), + GasTip: big.NewInt(1500000000), + GasCap: big.NewInt(1501000000), Nonce: 1, - To: "0x57B414a0332B5CaB885a451c2a28a07d1e9b8a8d", - Value: big.NewInt(42894881044106498), + To: constructionToAddress, + Value: big.NewInt(1000), } - var gasPrice *big.Int = nil + var blockNum *big.Int = nil mockClient.On( - "SuggestGasPrice", + "BlockHeader", + ctx, + blockNum, + ).Return( + &header, + nil, + ).Once() + mockClient.On( + "SuggestGasTipCap", ctx, - gasPrice, ).Return( - big.NewInt(1000000000), + big.NewInt(1500000000), nil, ).Once() metadataResponse, err := servicer.ConstructionMetadata(ctx, &types.ConstructionMetadataRequest{ @@ -358,21 +402,21 @@ func TestConstructionFlowWithInputNonce(t *testing.T) { Metadata: forceMarshalMap(t, metadata), SuggestedFee: []*types.Amount{ { - Value: "21000000000000", + Value: "31510500000000", Currency: polygon.Currency, }, }, }, metadataResponse) // Test Payloads - unsignedRaw := `{"from":"0xD10a72Cf054650931365Cc44D912a4FD75257058","to":"0x57B414a0332B5CaB885a451c2a28a07d1e9b8a8d","value":"0x9864aac3510d02","data":"0x","nonce":"0x1","gas_price":"0x3b9aca00","gas":"0x5208","chain_id":"0x13881"}` // nolint + unsignedRaw := `{"from":"0x5aCB42b3cfCD734a57AFF800139ba1354b549159","to":"0x3Fa177c2E87Cb24148EC403921dB577d140CC07c","value":"0x3e8","data":"0x","nonce":"0x1","max_fee_per_gas":"0x59777140","max_priority_fee_per_gas":"0x59682f00","gas":"0x5208","chain_id":"0x13881"}` payloadsResponse, err := servicer.ConstructionPayloads(ctx, &types.ConstructionPayloadsRequest{ NetworkIdentifier: networkIdentifier, Operations: ops, Metadata: forceMarshalMap(t, metadata), }) assert.Nil(t, err) - payloadsRaw := `[{"address":"0xD10a72Cf054650931365Cc44D912a4FD75257058","hex_bytes":"9fc67756448ac9767dd028418bc7970d843ffe283ea7b2a96e33392c3e13d3a8","account_identifier":{"address":"0xD10a72Cf054650931365Cc44D912a4FD75257058"},"signature_type":"ecdsa_recovery"}]` // nolint + payloadsRaw := `[{"address":"0x5aCB42b3cfCD734a57AFF800139ba1354b549159","hex_bytes":"2fbbd3c6a16a992785dbb6d6f3589d26dbc277aa83657b130b960c0da2422670","account_identifier":{"address":"0x5aCB42b3cfCD734a57AFF800139ba1354b549159"},"signature_type":"ecdsa_recovery"}]` var payloads []*types.SigningPayload assert.NoError(t, json.Unmarshal([]byte(payloadsRaw), &payloads)) assert.Equal(t, &types.ConstructionPayloadsResponse{ @@ -381,7 +425,7 @@ func TestConstructionFlowWithInputNonce(t *testing.T) { }, payloadsResponse) // Test Parse Unsigned - parseOpsRaw := `[{"operation_identifier":{"index":0},"type":"CALL","account":{"address":"0xD10a72Cf054650931365Cc44D912a4FD75257058"},"amount":{"value":"-42894881044106498","currency":{"symbol":"MATIC","decimals":18}}},{"operation_identifier":{"index":1},"related_operations":[{"index":0}],"type":"CALL","account":{"address":"0x57B414a0332B5CaB885a451c2a28a07d1e9b8a8d"},"amount":{"value":"42894881044106498","currency":{"symbol":"MATIC","decimals":18}}}]` // nolint + parseOpsRaw := `[{"operation_identifier":{"index":0},"type":"CALL","account":{"address":"0x5aCB42b3cfCD734a57AFF800139ba1354b549159"},"amount":{"value":"-1000","currency":{"symbol":"MATIC","decimals":18}}},{"operation_identifier":{"index":1},"related_operations":[{"index":0}],"type":"CALL","account":{"address":"0x3Fa177c2E87Cb24148EC403921dB577d140CC07c"},"amount":{"value":"1000","currency":{"symbol":"MATIC","decimals":18}}}]` var parseOps []*types.Operation assert.NoError(t, json.Unmarshal([]byte(parseOpsRaw), &parseOps)) parseUnsignedResponse, err := servicer.ConstructionParse(ctx, &types.ConstructionParseRequest{ @@ -392,8 +436,9 @@ func TestConstructionFlowWithInputNonce(t *testing.T) { assert.Nil(t, err) parseMetadata := &parseMetadata{ Nonce: metadata.Nonce, - GasPrice: metadata.GasPrice, GasLimit: metadata.GasLimit, + GasCap: metadata.GasCap, + GasTip: metadata.GasTip, ChainID: big.NewInt(80001), } assert.Equal(t, &types.ConstructionParseResponse{ @@ -403,10 +448,10 @@ func TestConstructionFlowWithInputNonce(t *testing.T) { }, parseUnsignedResponse) // Test Combine - signaturesRaw := `[{"hex_bytes":"f41bbaff27975ce07e5ab216c33f02384ef79290ef83537c452a61827d80d51a73e5a4707d46f03f4a5841de0623b744942e9edf6e0ef3761acdc40c7f147dc301","signing_payload":{"address":"0xD10a72Cf054650931365Cc44D912a4FD75257058","hex_bytes":"9fc67756448ac9767dd028418bc7970d843ffe283ea7b2a96e33392c3e13d3a8","account_identifier":{"address":"0xD10a72Cf054650931365Cc44D912a4FD75257058"},"signature_type":"ecdsa_recovery"},"public_key":{"hex_bytes":"0212e9f98d9750e5f74b4b4b00df39074f86c79187943bdb3c5a9c89ffc1ed0188","curve_type":"secp256k1"},"signature_type":"ecdsa_recovery"}]` // nolint + signaturesRaw := `[{"hex_bytes":"188597fb9e64a6875ceb033cd55910add342dc9ea5130f8c95010cd74366dc217de32a0d4b3da60dcce83f44b43cd9e733ded9c5da53a4d08b662b8d9e1c32c100","public_key":{"hex_bytes":"0405e82ac561143aafc13ba109677a597c8f797b07417d0addd7a346ad35882b3c4a006620e02127b9a32e90979ff93ecad0a2f577db238163a50023e393e354ff","curve_type":"secp256k1"},"signing_payload":{"hex_bytes":"2fbbd3c6a16a992785dbb6d6f3589d26dbc277aa83657b130b960c0da2422670","address":"0x5aCB42b3cfCD734a57AFF800139ba1354b549159"},"signature_type":"ecdsa_recovery"}]` var signatures []*types.Signature assert.NoError(t, json.Unmarshal([]byte(signaturesRaw), &signatures)) - signedRaw := `{"type":"0x0","nonce":"0x1","gasPrice":"0x3b9aca00","maxPriorityFeePerGas":null,"maxFeePerGas":null,"gas":"0x5208","value":"0x9864aac3510d02","input":"0x","v":"0x27126","r":"0xf41bbaff27975ce07e5ab216c33f02384ef79290ef83537c452a61827d80d51a","s":"0x73e5a4707d46f03f4a5841de0623b744942e9edf6e0ef3761acdc40c7f147dc3","to":"0x57b414a0332b5cab885a451c2a28a07d1e9b8a8d","hash":"0x06c3e9f1e4d6309b33e86aeb91b0b64d58057246d5d1fb4302bbb3fe2c745b3c"}` // nolint + signedRaw := `{"type":"0x2","nonce":"0x1","gasPrice":null,"maxPriorityFeePerGas":"0x59682f00","maxFeePerGas":"0x59777140","gas":"0x5208","value":"0x3e8","input":"0x","v":"0x0","r":"0x188597fb9e64a6875ceb033cd55910add342dc9ea5130f8c95010cd74366dc21","s":"0x7de32a0d4b3da60dcce83f44b43cd9e733ded9c5da53a4d08b662b8d9e1c32c1","to":"0x3fa177c2e87cb24148ec403921db577d140cc07c","chainId":"0x13881","accessList":[],"hash":"0x69589b9f54bfe4a25c807c9d4dac1ebf305b7e806437ebe4cc113b7847439aab"}` // nolint combineResponse, err := servicer.ConstructionCombine(ctx, &types.ConstructionCombineRequest{ NetworkIdentifier: networkIdentifier, UnsignedTransaction: unsignedRaw, @@ -418,6 +463,8 @@ func TestConstructionFlowWithInputNonce(t *testing.T) { }, combineResponse) // Test Parse Signed + var parseSignedOps []*types.Operation + assert.NoError(t, json.Unmarshal([]byte(parseOpsRaw), &parseSignedOps)) parseSignedResponse, err := servicer.ConstructionParse(ctx, &types.ConstructionParseRequest{ NetworkIdentifier: networkIdentifier, Signed: true, @@ -425,16 +472,16 @@ func TestConstructionFlowWithInputNonce(t *testing.T) { }) assert.Nil(t, err) assert.Equal(t, &types.ConstructionParseResponse{ - Operations: parseOps, + Operations: parseSignedOps, AccountIdentifierSigners: []*types.AccountIdentifier{ - {Address: "0xD10a72Cf054650931365Cc44D912a4FD75257058"}, + {Address: constructionFromAddress}, }, Metadata: forceMarshalMap(t, parseMetadata), }, parseSignedResponse) // Test Hash transactionIdentifier := &types.TransactionIdentifier{ - Hash: "0x06c3e9f1e4d6309b33e86aeb91b0b64d58057246d5d1fb4302bbb3fe2c745b3c", + Hash: "0x69589b9f54bfe4a25c807c9d4dac1ebf305b7e806437ebe4cc113b7847439aab", } hashResponse, err := servicer.ConstructionHash(ctx, &types.ConstructionHashRequest{ NetworkIdentifier: networkIdentifier, diff --git a/services/construction/metadata.go b/services/construction/metadata.go index f2da06c..2001867 100644 --- a/services/construction/metadata.go +++ b/services/construction/metadata.go @@ -81,7 +81,6 @@ func (a *APIService) ConstructionMetadata( } // Override the destination address to be the contract address to = checkTokenContractAddress - var err *types.Error gasLimit, err = a.calculateGasLimit(ctx, checkFrom, checkTokenContractAddress, input.Data, input.Value, input.GasLimit) if err != nil { @@ -108,16 +107,31 @@ func (a *APIService) ConstructionMetadata( } } - // TODO: Upgrade to use EIP1559 - gasPrice, err := a.client.SuggestGasPrice(ctx, input.GasPrice) + header, err := a.client.BlockHeader(ctx, nil) if err != nil { return nil, svcErrors.WrapErr(svcErrors.ErrGeth, err) } + gasTip, err := a.client.SuggestGasTipCap(ctx) + if err != nil { + return nil, svcErrors.WrapErr(svcErrors.ErrGeth, err) + } + + var gasCap *big.Int + if input.GasCap == nil { + // Set default max fee to double the last base fee plus priority tip + // to ensure tx is highly likely to go out in the next block + multiplier := big.NewInt(2) + gasCap = new(big.Int).Add(gasTip, new(big.Int).Mul(header.BaseFee, multiplier)) + } else { + gasCap = input.GasCap + } + metadata := &metadata{ Nonce: nonce, - GasPrice: gasPrice, GasLimit: gasLimit, + GasCap: gasCap, + GasTip: gasTip, Data: input.Data, Value: input.Value, To: to, @@ -131,7 +145,7 @@ func (a *APIService) ConstructionMetadata( } // Find suggested gas usage - suggestedFee := metadata.GasPrice.Int64() * int64(gasLimit) + suggestedFee := (header.BaseFee.Int64() + metadata.GasTip.Int64()) * int64(gasLimit) return &types.ConstructionMetadataResponse{ Metadata: metadataMap, diff --git a/services/construction/metadata_test.go b/services/construction/metadata_test.go index 215e84e..d32af82 100644 --- a/services/construction/metadata_test.go +++ b/services/construction/metadata_test.go @@ -39,6 +39,7 @@ var ( metadataGenericData = "0x095ea7b3000000000000000000000000d10a72cf054650931365cc44d912a4fd7525705800000000000000000000000000000000000000000000000000000000000003e8" maticTokenContract = "0x0000000000000000000000000000000000001010" metadataMaticWithdrawData = "0x2e1a7d4d0000000000000000000000000000000000000000000000000000000005f5e100" + ) func TestMetadata_Offline(t *testing.T) { @@ -75,21 +76,26 @@ func TestMetadata(t *testing.T) { "to": metadataTo, "value": transferValueHex, "nonce": transferNonceHex2, - "gas_price": transferGasPriceHex, "gas_limit": transferGasLimitHex, + "gas_cap": transferGasCapHex, + "gas_tip": transferGasTipHex, }, SuggestedFee: []*types.Amount{ { - Value: fmt.Sprintf("%d", transferGasPrice*transferGasLimit), + Value: fmt.Sprintf("%d", (header.BaseFee.Uint64() + transferGasTip) * transferGasLimit), Currency: polygon.Currency, }, }, }, mocks: func(ctx context.Context, client *mocks.Client) { - var gasPrice *big.Int = nil + var blockNum *big.Int = nil + + client.On("BlockHeader", ctx, blockNum). + Return(&header, nil) + + client.On("SuggestGasTipCap", ctx). + Return(big.NewInt(int64(transferGasTip)), nil) - client.On("SuggestGasPrice", ctx, gasPrice). - Return(big.NewInt(int64(transferGasPrice)), nil) }, }, "happy path: native currency without nonce": { @@ -99,25 +105,31 @@ func TestMetadata(t *testing.T) { "value": transferValueHex, }, mocks: func(ctx context.Context, client *mocks.Client) { - var gasPrice *big.Int = nil client.On("PendingNonceAt", ctx, common.HexToAddress(metadataFrom)). Return(transferNonce, nil) - client.On("SuggestGasPrice", ctx, gasPrice). - Return(big.NewInt(int64(transferGasPrice)), nil) + var blockNum *big.Int = nil + + client.On("BlockHeader", ctx, blockNum). + Return(&header, nil) + + client.On("SuggestGasTipCap", ctx). + Return(big.NewInt(int64(transferGasTip)), nil) + }, expectedResponse: &types.ConstructionMetadataResponse{ Metadata: map[string]interface{}{ "to": metadataTo, "value": transferValueHex, "nonce": transferNonceHex, - "gas_price": transferGasPriceHex, "gas_limit": transferGasLimitHex, + "gas_cap": transferGasCapHex, + "gas_tip": transferGasTipHex, }, SuggestedFee: []*types.Amount{ { - Value: fmt.Sprintf("%d", transferGasPrice*transferGasLimit), + Value: fmt.Sprintf("%d", (header.BaseFee.Uint64() + transferGasTip) * transferGasLimit), Currency: polygon.Currency, }, }, @@ -133,7 +145,6 @@ func TestMetadata(t *testing.T) { "data": metadataData, }, mocks: func(ctx context.Context, client *mocks.Client) { - var gasPrice *big.Int = nil to := common.HexToAddress(tokenContractAddress) dataBytes, _ := hexutil.Decode(metadataData) @@ -143,21 +154,28 @@ func TestMetadata(t *testing.T) { Data: dataBytes, }).Return(transferGasLimitERC20, nil) - client.On("SuggestGasPrice", ctx, gasPrice). - Return(big.NewInt(int64(transferGasPrice)), nil) + var blockNum *big.Int = nil + + client.On("BlockHeader", ctx, blockNum). + Return(&header, nil) + + client.On("SuggestGasTipCap", ctx). + Return(big.NewInt(int64(transferGasTip)), nil) + }, expectedResponse: &types.ConstructionMetadataResponse{ Metadata: map[string]interface{}{ "to": tokenContractAddress, "value": "0x0", "nonce": transferNonceHex2, - "gas_price": transferGasPriceHex, "gas_limit": transferGasLimitERC20Hex, + "gas_cap": transferGasCapHex, + "gas_tip": transferGasTipHex, "data": metadataData, }, SuggestedFee: []*types.Amount{ { - Value: fmt.Sprintf("%d", transferGasPrice*transferGasLimitERC20), + Value: fmt.Sprintf("%d", (header.BaseFee.Uint64() + transferGasTip) * transferGasLimitERC20), Currency: polygon.Currency, }, }, @@ -175,7 +193,6 @@ func TestMetadata(t *testing.T) { "method_args": []string{"0xD10a72Cf054650931365Cc44D912a4FD75257058", "1000"}, }, mocks: func(ctx context.Context, client *mocks.Client) { - var gasPrice *big.Int = nil to := common.HexToAddress(tokenContractAddress) dataBytes, _ := hexutil.Decode(metadataGenericData) @@ -185,23 +202,30 @@ func TestMetadata(t *testing.T) { Data: dataBytes, }).Return(transferGasLimitERC20, nil) - client.On("SuggestGasPrice", ctx, gasPrice). - Return(big.NewInt(int64(transferGasPrice)), nil) + var blockNum *big.Int = nil + + client.On("BlockHeader", ctx, blockNum). + Return(&header, nil) + + client.On("SuggestGasTipCap", ctx). + Return(big.NewInt(int64(transferGasTip)), nil) + }, expectedResponse: &types.ConstructionMetadataResponse{ Metadata: map[string]interface{}{ "to": tokenContractAddress, "value": "0x0", "nonce": transferNonceHex2, - "gas_price": transferGasPriceHex, "gas_limit": transferGasLimitERC20Hex, + "gas_cap": transferGasCapHex, + "gas_tip": transferGasTipHex, "data": metadataGenericData, "method_signature": "approve(address,uint256)", "method_args": []interface{}{"0xD10a72Cf054650931365Cc44D912a4FD75257058", "1000"}, }, SuggestedFee: []*types.Amount{ { - Value: fmt.Sprintf("%d", transferGasPrice*transferGasLimitERC20), + Value: fmt.Sprintf("%d", (header.BaseFee.Uint64() + transferGasTip) * transferGasLimitERC20), Currency: polygon.Currency, }, }, @@ -219,7 +243,6 @@ func TestMetadata(t *testing.T) { "method_args": []string{"100000000"}, }, mocks: func(ctx context.Context, client *mocks.Client) { - var gasPrice *big.Int = nil to := common.HexToAddress(maticTokenContract) dataBytes, _ := hexutil.Decode(metadataMaticWithdrawData) @@ -230,23 +253,29 @@ func TestMetadata(t *testing.T) { Value: big.NewInt(100000000), }).Return(transferGasLimitERC20, nil) - client.On("SuggestGasPrice", ctx, gasPrice). - Return(big.NewInt(int64(transferGasPrice)), nil) + var blockNum *big.Int = nil + + client.On("BlockHeader", ctx, blockNum). + Return(&header, nil) + + client.On("SuggestGasTipCap", ctx). + Return(big.NewInt(int64(transferGasTip)), nil) }, expectedResponse: &types.ConstructionMetadataResponse{ Metadata: map[string]interface{}{ "to": maticTokenContract, "value": "0x5f5e100", "nonce": transferNonceHex2, - "gas_price": transferGasPriceHex, "gas_limit": transferGasLimitERC20Hex, + "gas_cap": transferGasCapHex, + "gas_tip": transferGasTipHex, "data": metadataMaticWithdrawData, "method_signature": "withdraw(uint256)", "method_args": []interface{}{"100000000"}, }, SuggestedFee: []*types.Amount{ { - Value: fmt.Sprintf("%d", transferGasPrice*transferGasLimitERC20), + Value: fmt.Sprintf("%d", (header.BaseFee.Uint64() + transferGasTip) * transferGasLimitERC20), Currency: polygon.Currency, }, }, diff --git a/services/construction/parse.go b/services/construction/parse.go index 109afad..a51c15c 100644 --- a/services/construction/parse.go +++ b/services/construction/parse.go @@ -38,6 +38,7 @@ func (a *APIService) ConstructionParse( } } else { t := new(ethTypes.Transaction) + err := t.UnmarshalJSON([]byte(request.Transaction)) if err != nil { return nil, svcErrors.WrapErr(svcErrors.ErrUnableToParseIntermediateResult, err) @@ -47,11 +48,12 @@ func (a *APIService) ConstructionParse( tx.Value = t.Value() tx.Data = t.Data() tx.Nonce = t.Nonce() - tx.GasPrice = t.GasPrice() + tx.GasCap = t.GasFeeCap() + tx.GasTip = t.GasTipCap() tx.GasLimit = t.Gas() tx.ChainID = t.ChainId() - msg, err := t.AsMessage(ethTypes.NewEIP155Signer(t.ChainId()), nil) + msg, err := t.AsMessage(ethTypes.NewLondonSigner(t.ChainId()), nil) if err != nil { return nil, svcErrors.WrapErr(svcErrors.ErrUnableToParseIntermediateResult, err) } @@ -100,7 +102,8 @@ func (a *APIService) ConstructionParse( metadata := &parseMetadata{ Nonce: tx.Nonce, - GasPrice: tx.GasPrice, + GasCap: tx.GasCap, + GasTip: tx.GasTip, GasLimit: tx.GasLimit, ChainID: tx.ChainID, } @@ -126,6 +129,7 @@ func (a *APIService) ConstructionParse( AccountIdentifierSigners: []*types.AccountIdentifier{}, Metadata: metaMap, } + } return resp, nil } diff --git a/services/construction/parse_test.go b/services/construction/parse_test.go index 9fdfc78..e269286 100644 --- a/services/construction/parse_test.go +++ b/services/construction/parse_test.go @@ -16,6 +16,7 @@ package construction import ( "context" + "math/big" "testing" svcError "github.com/maticnetwork/polygon-rosetta/services/errors" @@ -27,13 +28,28 @@ import ( ) var ( - unsignedMaticTransferTx = `{"from":"0x966fbC4E1F3a938Cf7798695C3244d9C7C190015","to":"0xefD3dc58D60aF3295B92ecd484CAEB3A2f30b3e7","value":"0x134653c","data":"0x","nonce":"0x43","gas_price":"0x12a05f200","gas":"0x5208","chain_id":"0x13881"}` //nolint:lll - signedMaticTransferTx = `{"nonce":"0x43","gasPrice":"0x12a05f200","gas":"0x5208","to":"0xefd3dc58d60af3295b92ecd484caeb3a2f30b3e7","value":"0x134653c","input":"0x","v":"0x27125","r":"0x733a6097719aab45c9209c77e967f057c60036360d839a55316eaec60dbedcd9","s":"0x1fe4a59a206403cd09e0ff5b29f5062abb784c003590f84b7bb3daa4e0ade039","hash":"0xa4984c3f6767ec4465f4b11652a3b60fed1f006096f381aba5cf4800a30c5a53"}` //nolint:lll - unsignedERC20TransferTx = `{"from":"0x966fbC4E1F3a938Cf7798695C3244d9C7C190015","to":"0x2d7882beDcbfDDce29Ba99965dd3cdF7fcB10A1e","value":"0x0","data":"0xa9059cbb000000000000000000000000efd3dc58d60af3295b92ecd484caeb3a2f30b3e7000000000000000000000000000000000000000000000000000000000134653c","nonce":"0x43","gas_price":"0x12a05f200","gas":"0xfde8","chain_id":"0x13881"}` //nolint:lll - signedERC20TransferTx = `{"nonce":"0x43","gasPrice":"0x12a05f200","gas":"0xfde8","to":"0x2d7882bedcbfddce29ba99965dd3cdf7fcb10a1e","value":"0x0","input":"0xa9059cbb000000000000000000000000efd3dc58d60af3295b92ecd484caeb3a2f30b3e7000000000000000000000000000000000000000000000000000000000134653c","v":"0x27126","r":"0x66705f88684114cedeaa1d3dca1f1613591e1dae270cd3eafcaaa7c772c28093","s":"0x4e7f3d52f236cf80af661f4465416ac6954f0e65f60f4644bc97f2085e439fd7","hash":"0xcc3fb58789635d41d025d57ca3d973354bdb136b1812e63df6f0e9912ed1c608"}` //nolint:lll - unsignedERC20TransferTxInvalidData = `{"from":"0x966fbC4E1F3a938Cf7798695C3244d9C7C190015","to":"0x2d7882beDcbfDDce29Ba99965dd3cdF7fcB10A1e","value":"0x0","data":"0xaaaaaaaa000000000000000000000000efd3dc58d60af3295b92ecd484caeb3a2f30b3e7000000000000000000000000000000000000000000000000000000000134653c","nonce":"0x43","gas_price":"0x12a05f200","gas":"0xfde8","chain_id":"0x13881"}` //nolint:lll - unsignedMaticTransferTxInvalidFrom = `{"from":"invalid_from","to":"0xefD3dc58D60aF3295B92ecd484CAEB3A2f30b3e7","value":"0x134653c","data":"0x","nonce":"0x43","gas_price":"0x12a05f200","gas":"0x5208","chain_id":"0x13881"}` //nolint:lll - unsignedMaticTransferTxInvalidTo = `{"from":"0x966fbC4E1F3a938Cf7798695C3244d9C7C190015","to":"invalid_to","value":"0x134653c","data":"0x","nonce":"0x43","gas_price":"0x12a05f200","gas":"0x5208","chain_id":"0x13881"}` //nolint:lll + unsignedMaticTransferTx = `{"from":"0x5aCB42b3cfCD734a57AFF800139ba1354b549159","to":"0x3Fa177c2E87Cb24148EC403921dB577d140CC07c","value":"0x3e8","data":"0x","nonce":"0x2","max_fee_per_gas":"0x59682f15","max_priority_fee_per_gas":"0x59682eff","gas":"0x5208","chain_id":"0x13881"}` //nolint:lll + signedMaticTransferTx = `{"type":"0x2","nonce":"0x2","gasPrice":null,"maxPriorityFeePerGas":"0x59682eff","maxFeePerGas":"0x59682f15","gas":"0x5208","value":"0x3e8","input":"0x","v":"0x1","r":"0x6afe2f65d311ff2430ca7388335b86e42606ea4728924d91564405df83d2cea5","s":"0x443a04f2d96ea9877ed67f2b45266446ab01de2154c268470f57bb12effa1563","to":"0x3fa177c2e87cb24148ec403921db577d140cc07c","chainId":"0x13881","accessList":[],"hash":"0x554c2edbd04b2be9d1314ef31201e3382eedb24a733f1b15448af2d16252db73"}` //nolint:lll + unsignedERC20TransferTx = `{"from":"0x5aCB42b3cfCD734a57AFF800139ba1354b549159","to":"0x2d7882beDcbfDDce29Ba99965dd3cdF7fcB10A1e","value":"0x0","data":"0xa9059cbb0000000000000000000000003fa177c2e87cb24148ec403921db577d140cc07c0000000000000000000000000000000000000000000000000000000000000064","nonce":"0x2","max_fee_per_gas":"0x9502f914","max_priority_fee_per_gas":"0x9502f900","gas":"0xb2cb","chain_id":"0x13881"}` //nolint:lll + signedERC20TransferTx = `{"type":"0x2","nonce":"0x2","gasPrice":null,"maxPriorityFeePerGas":"0x9502f900","maxFeePerGas":"0x9502f914","gas":"0xb2cb","value":"0x0","input":"0xa9059cbb0000000000000000000000003fa177c2e87cb24148ec403921db577d140cc07c0000000000000000000000000000000000000000000000000000000000000064","v":"0x1","r":"0x2a8799b115741f62d5da931a53428ad1e3bf3055e9ea8427ce196a44cc590fca","s":"0x4779ab01b496c8b27e19efd24817557609b50da0d7e1a3790c435ca2225b43ae","to":"0x2d7882bedcbfddce29ba99965dd3cdf7fcb10a1e","chainId":"0x13881","accessList":[],"hash":"0xaa0f2056a79315e60a2012aee5f582692817e12153c6e45f57215f848893ec9e"}` //nolint:lll + unsignedERC20TransferTxInvalidData = `"{"from":"0x5aCB42b3cfCD734a57AFF800139ba1354b549159","to":"0x2d7882beDcbfDDce29Ba99965dd3cdF7fcB10A1e","value":"0x0","data":"0xaaaaaaaa000000000000000000000000efd3dc58d60af3295b92ecd484caeb3a2f30b3e7000000000000000000000000000000000000000000000000000000000134653c","nonce":"0x2","max_fee_per_gas":"0x9502f914","max_priority_fee_per_gas":"0x9502f900","gas":"0xb2cb","chain_id":"0x13881"}"` //nolint:lll + unsignedMaticTransferTxInvalidFrom = `{"from":"invalid_from","to":"0x3Fa177c2E87Cb24148EC403921dB577d140CC07c","value":"0x3e8","data":"0x","nonce":"0x2","max_fee_per_gas":"0x59682f15","max_priority_fee_per_gas":"0x59682eff","gas":"0x5208","chain_id":"0x13881"}` //nolint:lll + unsignedMaticTransferTxInvalidTo = `{"from":"0x5aCB42b3cfCD734a57AFF800139ba1354b549159","to":"invalid_to","value":"0x3e8","data":"0x","nonce":"0x2","max_fee_per_gas":"0x59682f15","max_priority_fee_per_gas":"0x59682eff","gas":"0x5208","chain_id":"0x13881"}` //nolint:lll + + parseFromAddress = "0x5aCB42b3cfCD734a57AFF800139ba1354b549159" + parseToAddress = "0x3Fa177c2E87Cb24148EC403921dB577d140CC07c" + parseTokenContractAddress = "0x2d7882beDcbfDDce29Ba99965dd3cdF7fcB10A1e" + + gasCapHex = "0x59682f15" + gasTipHex = "0x59682eff" + nonceHex = "0x2" + value = uint64(1000) + + gasCapERC20Hex = "0x9502f914" + gasTipERC20Hex = "0x9502f900" + gasLimitERC20Hex = "0xb2cb" + nonceERC20Hex = "0x2" + valueERC20 = uint64(100) ) func TestParse(t *testing.T) { @@ -49,12 +65,13 @@ func TestParse(t *testing.T) { Transaction: unsignedMaticTransferTx, }, expectedResponse: &types.ConstructionParseResponse{ - Operations: templateOperations(transferValue, polygon.Currency), + Operations: parseTemplateOperations(parseFromAddress, parseToAddress, value, polygon.Currency), AccountIdentifierSigners: []*types.AccountIdentifier{}, Metadata: map[string]interface{}{ - "nonce": transferNonceHex, - "gas_price": transferGasPriceHex, + "nonce": nonceHex, "gas_limit": transferGasLimitHex, + "gas_cap": gasCapHex, + "gas_tip": gasTipHex, "chain_id": chainIDHex, }, }, @@ -66,16 +83,17 @@ func TestParse(t *testing.T) { Transaction: signedMaticTransferTx, }, expectedResponse: &types.ConstructionParseResponse{ - Operations: templateOperations(transferValue, polygon.Currency), + Operations: parseTemplateOperations(parseFromAddress, parseToAddress, value, polygon.Currency), AccountIdentifierSigners: []*types.AccountIdentifier{ { - Address: fromAddress, + Address: parseFromAddress, }, }, Metadata: map[string]interface{}{ - "nonce": transferNonceHex, - "gas_price": transferGasPriceHex, + "nonce": nonceHex, "gas_limit": transferGasLimitHex, + "gas_cap": gasCapHex, + "gas_tip": gasTipHex, "chain_id": chainIDHex, }, }, @@ -87,18 +105,19 @@ func TestParse(t *testing.T) { Transaction: unsignedERC20TransferTx, }, expectedResponse: &types.ConstructionParseResponse{ - Operations: templateOperations(transferValue, &types.Currency{ + Operations: parseTemplateOperations(parseFromAddress, parseToAddress, valueERC20, &types.Currency{ Symbol: "ERC20", Decimals: 18, Metadata: map[string]interface{}{ - "token_address": tokenContractAddress, + "token_address": parseTokenContractAddress, }, }), AccountIdentifierSigners: []*types.AccountIdentifier{}, Metadata: map[string]interface{}{ - "nonce": transferNonceHex, - "gas_price": transferGasPriceHex, - "gas_limit": transferGasLimitERC20Hex, + "nonce": nonceERC20Hex, + "gas_limit": gasLimitERC20Hex, + "gas_cap": gasCapERC20Hex, + "gas_tip": gasTipERC20Hex, "chain_id": chainIDHex, }, }, @@ -110,22 +129,23 @@ func TestParse(t *testing.T) { Transaction: signedERC20TransferTx, }, expectedResponse: &types.ConstructionParseResponse{ - Operations: templateOperations(transferValue, &types.Currency{ + Operations: parseTemplateOperations(parseFromAddress, parseToAddress, valueERC20, &types.Currency{ Symbol: "ERC20", Decimals: 18, Metadata: map[string]interface{}{ - "token_address": tokenContractAddress, + "token_address": parseTokenContractAddress, }, }), AccountIdentifierSigners: []*types.AccountIdentifier{ { - Address: fromAddress, + Address: parseFromAddress, }, }, Metadata: map[string]interface{}{ - "nonce": transferNonceHex, - "gas_price": transferGasPriceHex, - "gas_limit": transferGasLimitERC20Hex, + "nonce": nonceERC20Hex, + "gas_limit": gasLimitERC20Hex, + "gas_cap": gasCapERC20Hex, + "gas_tip": gasTipERC20Hex, "chain_id": chainIDHex, }, }, @@ -182,3 +202,12 @@ func TestParse(t *testing.T) { }) } } + +func parseTemplateOperations(fromAddress string, toAddress string, amount uint64, currency *types.Currency) []*types.Operation { + return rosettaOperations( + fromAddress, + toAddress, + big.NewInt(int64(amount)), + currency, + ) +} diff --git a/services/construction/payloads.go b/services/construction/payloads.go index 0e4308e..9463d40 100644 --- a/services/construction/payloads.go +++ b/services/construction/payloads.go @@ -58,7 +58,8 @@ func (a *APIService) ConstructionPayloads( amount := metadata.Value toAdd := metadata.To nonce := metadata.Nonce - gasPrice := metadata.GasPrice + gasCap := metadata.GasCap + gasTip := metadata.GasTip chainID := a.config.Params.ChainID transferGasLimit := metadata.GasLimit transferData := metadata.Data @@ -75,14 +76,17 @@ func (a *APIService) ConstructionPayloads( return nil, svcErrors.WrapErr(svcErrors.ErrInvalidAddress, fmt.Errorf("%s is not a valid address", toAdd)) } - tx := ethTypes.NewTransaction( - nonce, - common.HexToAddress(checkTo), - amount, - transferGasLimit, - gasPrice, - transferData, - ) + toAddress := common.HexToAddress(checkTo) + tx := ethTypes.NewTx( + ðTypes.DynamicFeeTx{ + Nonce: nonce, + To: &toAddress, + Value: amount, + Gas: transferGasLimit, + GasFeeCap: gasCap, + GasTipCap: gasTip, + Data: transferData, + }) unsignedTx := &transaction{ From: checkFrom, @@ -90,13 +94,14 @@ func (a *APIService) ConstructionPayloads( Value: amount, Data: tx.Data(), Nonce: tx.Nonce(), - GasPrice: gasPrice, GasLimit: tx.Gas(), + GasCap: tx.GasFeeCap(), + GasTip: tx.GasTipCap(), ChainID: chainID, } // Construct SigningPayload - signer := ethTypes.NewEIP155Signer(chainID) + signer := ethTypes.NewLondonSigner(chainID) payload := &types.SigningPayload{ AccountIdentifier: &types.AccountIdentifier{Address: checkFrom}, Bytes: signer.Hash(tx).Bytes(), diff --git a/services/construction/payloads_test.go b/services/construction/payloads_test.go index 442d59a..f696ac7 100644 --- a/services/construction/payloads_test.go +++ b/services/construction/payloads_test.go @@ -33,6 +33,7 @@ import ( var ( invalidTransferData = "0xaaaaaaaa000000000000000000000000efd3dc58d60af3295b92ecd484caeb3a2f30b3e70000000000000000000000000000000000000000000000000000000000000001" // nolint + ) func TestPayloads(t *testing.T) { @@ -48,7 +49,7 @@ func TestPayloads(t *testing.T) { ), expectedResponse: templateConstructionPayloadsResponse( templateNativeCurrencyUnsigned(), - "0xa5f38ab5ca0c7c398e90f44111c3aae0c02be5c3054dc8933174b9898a4dcdfd", + "0xc06f3a7b39de0be0497941cade621537240067f068385ecb958357f68e13db7d", ), }, "happy path: ERC20 currency": { @@ -64,7 +65,7 @@ func TestPayloads(t *testing.T) { ), expectedResponse: templateConstructionPayloadsResponse( templateERC20CurrencyUnsigned(), - "0x55bf0447d109c960db3290be9bf3893f7b2476fd71c23e85550dd55b4602ea23", + "0x636590d08514899553fc91879fa49ad56708b63ed3be858c161b57f42e5adbfd", ), }, "happy path: Generic contract call": { @@ -80,7 +81,7 @@ func TestPayloads(t *testing.T) { ), expectedResponse: templateConstructionPayloadsResponse( templateGenericContractCallUnsigned(), - "0x6db5acd2132d1eaa7cf47392b98ecee1befe2a760f2e68d7ef7e9df40f63a384", + "0x17edf049123b31a3d9c537f2a8b705838c2e0346a427f079a593c61a688d2fd3", ), }, "error: bad request: native currency mismatch destination address": { @@ -194,8 +195,9 @@ func templateNativeCurrencyTxMetadata(amount string) map[string]interface{} { "nonce": transferNonceHex, "to": metadataTo, "value": amount, - "gas_price": transferGasPriceHex, "gas_limit": transferGasLimitHex, + "gas_cap": transferGasCapHex, + "gas_tip": transferGasTipHex, } } @@ -204,21 +206,23 @@ func templateERC20CurrencyTxMetadata() map[string]interface{} { "nonce": transferNonceHex, "to": tokenContractAddress, "value": "0x0", - "gas_price": transferGasPriceHex, "gas_limit": transferGasLimitERC20Hex, + "gas_cap": transferGasCapHex, + "gas_tip": transferGasTipHex, "data": metadataData, } } func templateNativeCurrencyUnsigned() string { return fmt.Sprintf( - `{"from":"%s","to":"%s","value":"%s","data":"%s","nonce":"%s","gas_price":"%s","gas":"%s","chain_id":"%s"}`, //nolint:lll + `{"from":"%s","to":"%s","value":"%s","data":"%s","nonce":"%s","max_fee_per_gas":"%s","max_priority_fee_per_gas":"%s","gas":"%s","chain_id":"%s"}`, //nolint:lll metadataFrom, metadataTo, transferValueHex, "0x", transferNonceHex, - transferGasPriceHex, + transferGasCapHex, + transferGasTipHex, transferGasLimitHex, chainIDHex, ) @@ -226,13 +230,14 @@ func templateNativeCurrencyUnsigned() string { func templateERC20CurrencyUnsigned() string { return fmt.Sprintf( - `{"from":"%s","to":"%s","value":"%s","data":"%s","nonce":"%s","gas_price":"%s","gas":"%s","chain_id":"%s"}`, //nolint:lll + `{"from":"%s","to":"%s","value":"%s","data":"%s","nonce":"%s","max_fee_per_gas":"%s","max_priority_fee_per_gas":"%s","gas":"%s","chain_id":"%s"}`, //nolint:lll metadataFrom, tokenContractAddress, "0x0", metadataData, transferNonceHex, - transferGasPriceHex, + transferGasCapHex, + transferGasTipHex, transferGasLimitERC20Hex, chainIDHex, ) @@ -243,8 +248,9 @@ func templateGenericContractCallTxMetadata() map[string]interface{} { "nonce": transferNonceHex, "to": tokenContractAddress, "value": "0x0", - "gas_price": transferGasPriceHex, "gas_limit": transferGasLimitERC20Hex, + "gas_cap": transferGasCapHex, + "gas_tip": transferGasTipHex, "data": metadataGenericData, "method_signature": "approve(address,uint256)", "method_args": []interface{}{"0xD10a72Cf054650931365Cc44D912a4FD75257058", "1000"}, @@ -253,13 +259,14 @@ func templateGenericContractCallTxMetadata() map[string]interface{} { func templateGenericContractCallUnsigned() string { return fmt.Sprintf( - `{"from":"%s","to":"%s","value":"%s","data":"%s","nonce":"%s","gas_price":"%s","gas":"%s","chain_id":"%s"}`, //nolint:lll + `{"from":"%s","to":"%s","value":"%s","data":"%s","nonce":"%s","max_fee_per_gas":"%s","max_priority_fee_per_gas":"%s","gas":"%s","chain_id":"%s"}`, //nolint:lll metadataFrom, tokenContractAddress, "0x0", metadataGenericData, transferNonceHex, - transferGasPriceHex, + transferGasCapHex, + transferGasTipHex, transferGasLimitERC20Hex, chainIDHex, ) diff --git a/services/construction/preprocess.go b/services/construction/preprocess.go index 79d82e5..2718b44 100644 --- a/services/construction/preprocess.go +++ b/services/construction/preprocess.go @@ -95,23 +95,42 @@ func (a *APIService) ConstructionPreprocess( preprocessOutputOptions.Nonce = bigObj } - // Override gas_price - if v, ok := request.Metadata["gas_price"]; ok { + // Override gas_tip + if v, ok := request.Metadata["gas_tip"]; ok { stringObj, ok := v.(string) if !ok { return nil, svcErrors.WrapErr( - svcErrors.ErrInvalidGasPrice, - fmt.Errorf("%s is not a valid gas_price string", v), + svcErrors.ErrInvalidGasTip, + fmt.Errorf("%s is not a valid gas_tip string", v), ) } bigObj, ok := new(big.Int).SetString(stringObj, 10) //nolint:gomnd if !ok { return nil, svcErrors.WrapErr( - svcErrors.ErrInvalidGasPrice, - fmt.Errorf("%s is not a valid gas_price", v), + svcErrors.ErrInvalidGasTip, + fmt.Errorf("%s is not a valid gas_tip", v), ) } - preprocessOutputOptions.GasPrice = bigObj + preprocessOutputOptions.GasTip = bigObj + } + + // Override gas_cap + if v, ok := request.Metadata["gas_cap"]; ok { + stringObj, ok := v.(string) + if !ok { + return nil, svcErrors.WrapErr( + svcErrors.ErrInvalidGasCap, + fmt.Errorf("%s is not a valid gas_cap string", v), + ) + } + bigObj, ok := new(big.Int).SetString(stringObj, 10) //nolint:gomnd + if !ok { + return nil, svcErrors.WrapErr( + svcErrors.ErrInvalidGasCap, + fmt.Errorf("%s is not a valid gas_cap", v), + ) + } + preprocessOutputOptions.GasCap = bigObj } // Override gas_limit diff --git a/services/construction/preprocess_test.go b/services/construction/preprocess_test.go index 4277892..f822e44 100644 --- a/services/construction/preprocess_test.go +++ b/services/construction/preprocess_test.go @@ -35,10 +35,12 @@ var ( preprocessTransferValueHex = hexutil.EncodeUint64(preprocessTransferValue) preprocessTransferValueLargeHex = hexutil.EncodeUint64(preprocessTransferValueLargeValue) preprocessData = "0xa9059cbb000000000000000000000000efd3dc58d60af3295b92ecd484caeb3a2f30b3e70000000000000000000000000000000000000000000000000000000000000001" // nolint - preprocessGasPrice = uint64(100000000000) - preprocessGasPriceHex = hexutil.EncodeUint64(preprocessGasPrice) preprocessGasLimit = uint64(600000) preprocessGasLimitHex = hexutil.EncodeUint64(preprocessGasLimit) + preprocessGasTip = uint64(20000000) + preprocessGasTipHex = hexutil.EncodeUint64(preprocessGasTip) + preprocessGasCap = uint64(5000000000) + preprocessGasCapHex = hexutil.EncodeUint64(preprocessGasCap) preprocessGenericData = "0x095ea7b3000000000000000000000000d10a72cf054650931365cc44d912a4fd7525705800000000000000000000000000000000000000000000000000000000000003e8" methodSignature = "approve(address,uint256)" methodArgs = []string{"0xD10a72Cf054650931365Cc44D912a4FD75257058", "1000"} @@ -167,21 +169,21 @@ func TestPreprocess(t *testing.T) { }, }, }, - "happy path: native currency with gas price": { + "happy path: native currency with gas limit": { operations: templateOperations(preprocessTransferValue, polygon.Currency), metadata: map[string]interface{}{ - "gas_price": "100000000000", + "gas_limit": "600000", }, expectedResponse: &types.ConstructionPreprocessResponse{ Options: map[string]interface{}{ "from": preprocessFromAddress, "to": preprocessToAddress, "value": preprocessTransferValueHex, - "gas_price": preprocessGasPriceHex, + "gas_limit": preprocessGasLimitHex, }, }, }, - "happy path: ERC20 currency with gas price": { + "happy path: ERC20 currency with gas limit": { operations: templateOperations(preprocessTransferValue, &types.Currency{ Symbol: "USDC", Decimals: 18, @@ -190,7 +192,7 @@ func TestPreprocess(t *testing.T) { }, }), metadata: map[string]interface{}{ - "gas_price": "100000000000", + "gas_limit": "600000", }, expectedResponse: &types.ConstructionPreprocessResponse{ Options: map[string]interface{}{ @@ -199,25 +201,25 @@ func TestPreprocess(t *testing.T) { "value": "0x0", "token_address": preprocessTokenContractAddress, "data": preprocessData, - "gas_price": preprocessGasPriceHex, + "gas_limit": preprocessGasLimitHex, }, }, }, - "happy path: native currency with gas limit": { + "happy path: native currency with gas cap": { operations: templateOperations(preprocessTransferValue, polygon.Currency), metadata: map[string]interface{}{ - "gas_limit": "600000", + "gas_cap": "5000000000", }, expectedResponse: &types.ConstructionPreprocessResponse{ Options: map[string]interface{}{ "from": preprocessFromAddress, "to": preprocessToAddress, "value": preprocessTransferValueHex, - "gas_limit": preprocessGasLimitHex, + "gas_cap": preprocessGasCapHex, }, }, }, - "happy path: ERC20 currency with gas limit": { + "happy path: ERC20 currency with gas cap": { operations: templateOperations(preprocessTransferValue, &types.Currency{ Symbol: "USDC", Decimals: 18, @@ -226,7 +228,7 @@ func TestPreprocess(t *testing.T) { }, }), metadata: map[string]interface{}{ - "gas_limit": "600000", + "gas_cap": "5000000000", }, expectedResponse: &types.ConstructionPreprocessResponse{ Options: map[string]interface{}{ @@ -235,7 +237,43 @@ func TestPreprocess(t *testing.T) { "value": "0x0", "token_address": preprocessTokenContractAddress, "data": preprocessData, - "gas_limit": preprocessGasLimitHex, + "gas_cap": preprocessGasCapHex, + }, + }, + }, + "happy path: native currency with gas tip": { + operations: templateOperations(preprocessTransferValue, polygon.Currency), + metadata: map[string]interface{}{ + "gas_tip": "20000000", + }, + expectedResponse: &types.ConstructionPreprocessResponse{ + Options: map[string]interface{}{ + "from": preprocessFromAddress, + "to": preprocessToAddress, + "value": preprocessTransferValueHex, + "gas_tip": preprocessGasTipHex, + }, + }, + }, + "happy path: ERC20 currency with gas tip": { + operations: templateOperations(preprocessTransferValue, &types.Currency{ + Symbol: "USDC", + Decimals: 18, + Metadata: map[string]interface{}{ + "token_address": preprocessTokenContractAddress, + }, + }), + metadata: map[string]interface{}{ + "gas_tip": "20000000", + }, + expectedResponse: &types.ConstructionPreprocessResponse{ + Options: map[string]interface{}{ + "from": preprocessFromAddress, + "to": preprocessToAddress, + "value": "0x0", + "token_address": preprocessTokenContractAddress, + "data": preprocessData, + "gas_tip": preprocessGasTipHex, }, }, }, @@ -310,41 +348,59 @@ func TestPreprocess(t *testing.T) { expectedError: templateError( svcErrors.ErrInvalidNonce, "invalid_nonce is not a valid nonce"), }, - "error: invalid gas price string": { + "error: invalid gas limit string": { operations: templateOperations(preprocessTransferValue, polygon.Currency), metadata: map[string]interface{}{ - "gas_price": map[string]string{}, + "gas_limit": map[string]string{}, }, expectedResponse: nil, expectedError: templateError( - svcErrors.ErrInvalidGasPrice, "map[] is not a valid gas_price string"), + svcErrors.ErrInvalidGasLimit, "map[] is not a valid gas_limit string"), }, - "error: invalid gas price": { + "error: invalid gas limit": { operations: templateOperations(preprocessTransferValue, polygon.Currency), metadata: map[string]interface{}{ - "gas_price": "gas_price", + "gas_limit": "gas_limit", }, expectedResponse: nil, expectedError: templateError( - svcErrors.ErrInvalidGasPrice, "gas_price is not a valid gas_price"), + svcErrors.ErrInvalidGasLimit, "gas_limit is not a valid gas_limit"), }, - "error: invalid gas limit string": { + "error: invalid gas cap string": { operations: templateOperations(preprocessTransferValue, polygon.Currency), metadata: map[string]interface{}{ - "gas_limit": map[string]string{}, + "gas_cap": map[string]string{}, }, expectedResponse: nil, expectedError: templateError( - svcErrors.ErrInvalidGasLimit, "map[] is not a valid gas_limit string"), + svcErrors.ErrInvalidGasCap, "map[] is not a valid gas_cap string"), }, - "error: invalid gas limit": { + "error: invalid gas cap": { operations: templateOperations(preprocessTransferValue, polygon.Currency), metadata: map[string]interface{}{ - "gas_limit": "gas_limit", + "gas_cap": "gas_cap", }, expectedResponse: nil, expectedError: templateError( - svcErrors.ErrInvalidGasLimit, "gas_limit is not a valid gas_limit"), + svcErrors.ErrInvalidGasCap, "gas_cap is not a valid gas_cap"), + }, + "error: invalid gas tip string": { + operations: templateOperations(preprocessTransferValue, polygon.Currency), + metadata: map[string]interface{}{ + "gas_tip": map[string]string{}, + }, + expectedResponse: nil, + expectedError: templateError( + svcErrors.ErrInvalidGasTip, "map[] is not a valid gas_tip string"), + }, + "error: invalid gas tip": { + operations: templateOperations(preprocessTransferValue, polygon.Currency), + metadata: map[string]interface{}{ + "gas_tip": "gas_tip", + }, + expectedResponse: nil, + expectedError: templateError( + svcErrors.ErrInvalidGasTip, "gas_tip is not a valid gas_tip"), }, "error: missing token address": { operations: templateOperations(preprocessTransferValue, &types.Currency{ diff --git a/services/construction/types.go b/services/construction/types.go index b9da02d..64e413f 100644 --- a/services/construction/types.go +++ b/services/construction/types.go @@ -54,10 +54,12 @@ type Client interface { EstimateGas(ctx context.Context, msg ethereum.CallMsg) (uint64, error) - SuggestGasPrice(ctx context.Context, gasPrice *big.Int) (*big.Int, error) + BlockHeader(ctx context.Context, number *big.Int) (*ethTypes.Header, error) SendTransaction(ctx context.Context, tx *ethTypes.Transaction) error + SuggestGasTipCap(ctx context.Context) (*big.Int, error) + Call( ctx context.Context, request *types.CallRequest, @@ -78,8 +80,9 @@ type options struct { TokenAddress string `json:"token_address,omitempty"` ContractAddress string `json:"contract_address,omitempty"` Value *big.Int `json:"value,omitempty"` - GasPrice *big.Int `json:"gas_price,omitempty"` GasLimit *big.Int `json:"gas_limit,omitempty"` + GasCap *big.Int `json:"gas_cap,omitempty"` + GasTip *big.Int `json:"gas_tip,omitempty"` MethodSignature string `json:"method_signature,omitempty"` MethodArgs []string `json:"method_args,omitempty"` } @@ -92,8 +95,9 @@ type optionsWire struct { TokenAddress string `json:"token_address,omitempty"` ContractAddress string `json:"contract_address,omitempty"` Value string `json:"value,omitempty"` - GasPrice string `json:"gas_price,omitempty"` GasLimit string `json:"gas_limit,omitempty"` + GasCap string `json:"gas_cap,omitempty"` + GasTip string `json:"gas_tip,omitempty"` MethodSignature string `json:"method_signature,omitempty"` MethodArgs []string `json:"method_args,omitempty"` } @@ -120,14 +124,18 @@ func (o *options) MarshalJSON() ([]byte, error) { ow.Value = hexutil.EncodeBig(o.Value) } - if o.GasPrice != nil { - ow.GasPrice = hexutil.EncodeBig(o.GasPrice) - } - if o.GasLimit != nil { ow.GasLimit = hexutil.EncodeBig(o.GasLimit) } + if o.GasCap != nil { + ow.GasCap = hexutil.EncodeBig(o.GasCap) + } + + if o.GasTip != nil { + ow.GasTip = hexutil.EncodeBig(o.GasTip) + } + return json.Marshal(ow) } @@ -167,20 +175,28 @@ func (o *options) UnmarshalJSON(data []byte) error { o.Value = value } - if len(ow.GasPrice) > 0 { - gasPrice, err := hexutil.DecodeBig(ow.GasPrice) + if len(ow.GasLimit) > 0 { + gasLimit, err := hexutil.DecodeBig(ow.GasLimit) if err != nil { return err } - o.GasPrice = gasPrice + o.GasLimit = gasLimit } - if len(ow.GasLimit) > 0 { - gasLimit, err := hexutil.DecodeBig(ow.GasLimit) + if len(ow.GasCap) > 0 { + gasCap, err := hexutil.DecodeBig(ow.GasCap) if err != nil { return err } - o.GasLimit = gasLimit + o.GasCap = gasCap + } + + if len(ow.GasTip) > 0 { + gasTip, err := hexutil.DecodeBig(ow.GasTip) + if err != nil { + return err + } + o.GasTip = gasTip } return nil @@ -188,7 +204,8 @@ func (o *options) UnmarshalJSON(data []byte) error { type metadata struct { Nonce uint64 `json:"nonce"` - GasPrice *big.Int `json:"gas_price"` + GasCap *big.Int `json:"gas_cap"` + GasTip *big.Int `json:"gas_tip"` GasLimit uint64 `json:"gas_limit,omitempty"` Data []byte `json:"data,omitempty"` To string `json:"to,omitempty"` @@ -199,7 +216,8 @@ type metadata struct { type metadataWire struct { Nonce string `json:"nonce"` - GasPrice string `json:"gas_price"` + GasCap string `json:"gas_cap"` + GasTip string `json:"gas_tip"` GasLimit string `json:"gas_limit,omitempty"` Data string `json:"data,omitempty"` To string `json:"to,omitempty"` @@ -211,7 +229,8 @@ type metadataWire struct { func (m *metadata) MarshalJSON() ([]byte, error) { mw := &metadataWire{ Nonce: hexutil.Uint64(m.Nonce).String(), - GasPrice: hexutil.EncodeBig(m.GasPrice), + GasCap: hexutil.EncodeBig(m.GasCap), + GasTip: hexutil.EncodeBig(m.GasTip), To: m.To, MethodSignature: m.MethodSignature, MethodArgs: m.MethodArgs, @@ -243,12 +262,18 @@ func (m *metadata) UnmarshalJSON(data []byte) error { return err } - gasPrice, err := hexutil.DecodeBig(mw.GasPrice) + gasCap, err := hexutil.DecodeBig(mw.GasCap) + if err != nil { + return err + } + + gasTip, err := hexutil.DecodeBig(mw.GasTip) if err != nil { return err } - m.GasPrice = gasPrice + m.GasCap = gasCap + m.GasTip = gasTip m.Nonce = nonce m.To = mw.To m.MethodSignature = mw.MethodSignature @@ -283,23 +308,26 @@ func (m *metadata) UnmarshalJSON(data []byte) error { type parseMetadata struct { Nonce uint64 `json:"nonce"` - GasPrice *big.Int `json:"gas_price"` GasLimit uint64 `json:"gas_limit"` + GasCap *big.Int `json:"gas_cap"` + GasTip *big.Int `json:"gas_tip"` ChainID *big.Int `json:"chain_id"` } type parseMetadataWire struct { Nonce string `json:"nonce"` - GasPrice string `json:"gas_price"` GasLimit string `json:"gas_limit"` + GasCap string `json:"gas_cap"` + GasTip string `json:"gas_tip"` ChainID string `json:"chain_id"` } func (p *parseMetadata) MarshalJSON() ([]byte, error) { pmw := &parseMetadataWire{ Nonce: hexutil.Uint64(p.Nonce).String(), - GasPrice: hexutil.EncodeBig(p.GasPrice), GasLimit: hexutil.Uint64(p.GasLimit).String(), + GasCap: hexutil.EncodeBig(p.GasCap), + GasTip: hexutil.EncodeBig(p.GasTip), ChainID: hexutil.EncodeBig(p.ChainID), } @@ -312,7 +340,8 @@ type transaction struct { Value *big.Int `json:"value"` Data []byte `json:"data"` Nonce uint64 `json:"nonce"` - GasPrice *big.Int `json:"gas_price"` + GasCap *big.Int `json:"max_fee_per_gas"` + GasTip *big.Int `json:"max_priority_fee_per_gas"` GasLimit uint64 `json:"gas"` ChainID *big.Int `json:"chain_id"` } @@ -323,7 +352,8 @@ type transactionWire struct { Value string `json:"value"` Data string `json:"data"` Nonce string `json:"nonce"` - GasPrice string `json:"gas_price"` + GasCap string `json:"max_fee_per_gas"` + GasTip string `json:"max_priority_fee_per_gas"` GasLimit string `json:"gas"` ChainID string `json:"chain_id"` } @@ -335,8 +365,9 @@ func (t *transaction) MarshalJSON() ([]byte, error) { Value: hexutil.EncodeBig(t.Value), Data: hexutil.Encode(t.Data), Nonce: hexutil.EncodeUint64(t.Nonce), - GasPrice: hexutil.EncodeBig(t.GasPrice), GasLimit: hexutil.EncodeUint64(t.GasLimit), + GasCap: hexutil.EncodeBig(t.GasCap), + GasTip: hexutil.EncodeBig(t.GasTip), ChainID: hexutil.EncodeBig(t.ChainID), } @@ -344,6 +375,7 @@ func (t *transaction) MarshalJSON() ([]byte, error) { } func (t *transaction) UnmarshalJSON(data []byte) error { + var tw transactionWire if err := json.Unmarshal(data, &tw); err != nil { return err @@ -364,12 +396,16 @@ func (t *transaction) UnmarshalJSON(data []byte) error { return err } - gasPrice, err := hexutil.DecodeBig(tw.GasPrice) + gasLimit, err := hexutil.DecodeUint64(tw.GasLimit) if err != nil { return err } - gasLimit, err := hexutil.DecodeUint64(tw.GasLimit) + gasCap, err := hexutil.DecodeBig(tw.GasCap) + if err != nil { + return err + } + gasTip, err := hexutil.DecodeBig(tw.GasTip) if err != nil { return err } @@ -384,8 +420,9 @@ func (t *transaction) UnmarshalJSON(data []byte) error { t.Value = value t.Data = twData t.Nonce = nonce - t.GasPrice = gasPrice t.GasLimit = gasLimit + t.GasCap = gasCap + t.GasTip = gasTip t.ChainID = chainID return nil } diff --git a/services/errors/errors.go b/services/errors/errors.go index 723a614..8a6bb3e 100644 --- a/services/errors/errors.go +++ b/services/errors/errors.go @@ -196,19 +196,26 @@ var ( Message: "unable to parse the transaction", } - // ErrInvalidGasPrice is returned when input gas price - // is invalid. - ErrInvalidGasPrice = &types.Error{ - Code: 21, //nolint - Message: "Gas price invalid", - } - // ErrInvalidGasLimit is returned when input gas limit // is invalid. ErrInvalidGasLimit = &types.Error{ Code: 22, //nolint Message: "Gas limit invalid", } + + // ErrInvalidGasCap is returned when input gas cap + // is invalid. + ErrInvalidGasCap = &types.Error{ + Code: 23, //nolint + Message: "Gas cap invalid", + } + + // ErrInvalidGasTip is returned when input gas cap + // is invalid. + ErrInvalidGasTip = &types.Error{ + Code: 24, //nolint + Message: "Gas tip invalid", + } ) // WrapErr adds details to the types.Error provided. We use a function