From 7075fdf80566b8b2a3328b47a3d56b1f19d1efbf Mon Sep 17 00:00:00 2001 From: ash-krishnan Date: Fri, 3 Jun 2022 09:19:33 -0700 Subject: [PATCH] adding eip1-559 support --- mocks/polygon/graph_ql.go | 2 +- mocks/polygon/jsonrpc.go | 2 +- mocks/services/client.go | 48 +++++- polygon/client.go | 40 ++++- polygon/client_test.go | 2 +- services/construction/combine.go | 23 +-- services/construction/combine_test.go | 10 +- .../construction/construction_service_test.go | 147 +++++++++++++----- services/construction/metadata.go | 28 +++- services/construction/metadata_test.go | 106 ++++++++++--- services/construction/parse.go | 11 +- services/construction/parse_test.go | 84 +++++++--- services/construction/payloads.go | 25 +-- services/construction/payloads_test.go | 38 +++-- services/construction/preprocess.go | 19 +++ services/construction/preprocess_test.go | 56 +++++++ services/construction/types.go | 76 ++++++++- services/errors/errors.go | 7 + 18 files changed, 579 insertions(+), 145 deletions(-) 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..27cdf86 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) @@ -218,3 +241,26 @@ func (_m *Client) SuggestGasPrice(ctx context.Context, gasPrice *big.Int) (*big. return r0, r1 } + +// 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); ok { + r0 = rf(ctx) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(*big.Int) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(context.Context) error); ok { + r1 = rf(ctx) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/polygon/client.go b/polygon/client.go index 13eebef..fff1771 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,6 +194,27 @@ func (ec *Client) PendingNonceAt(ctx context.Context, account common.Address) (u return uint64(result), err } +// SuggestGasTipCap retrieves the currently suggested gas tip cap after 1559 to +// allow a timely execution of a transaction. +//func (ec *Client) GetFeeHistory(ctx context.Context) (*big.Int, error) { +// historicalBlocks := 4 +// var hex hexutil.Big +// if err := ec.c.CallContext(ctx, &hex, "eth_feeHistory", historicalBlocks, "pending", nil); err != nil { +// return nil, err +// } +// return (*big.Int)(&hex), 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_maxPriorityFeePerGas"); err != nil { + return nil, err + } + return (*big.Int)(&hex), nil +} + // 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) { @@ -255,7 +277,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 +313,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 { @@ -367,7 +391,7 @@ func (ec *Client) getBlock( txs[i] = tx.tx receipt := receipts[i] gasUsed := new(big.Int).SetUint64(receipt.GasUsed) - gasPrice, err := effectiveGasPrice(txs[i], head.BaseFee) + gasPrice, err := EffectiveGasPrice(txs[i], head.BaseFee) if err != nil { return nil, nil, fmt.Errorf("%w: failure getting gas price", err) } @@ -400,9 +424,9 @@ func (ec *Client) getBlock( return types.NewBlockWithHeader(&head).WithBody(txs, uncles), loadedTxs, nil } -// effectiveGasPrice returns the price of gas charged to this transaction to be included in the +// EffectiveGasPrice returns the price of gas charged to this transaction to be included in the // block. -func effectiveGasPrice(tx *EthTypes.Transaction, baseFee *big.Int) (*big.Int, error) { +func EffectiveGasPrice(tx *EthTypes.Transaction, baseFee *big.Int) (*big.Int, error) { if tx.Type() != eip1559TxType { return tx.GasPrice(), nil } @@ -1517,7 +1541,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..07a06f5 100644 --- a/polygon/client_test.go +++ b/polygon/client_test.go @@ -1447,7 +1447,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..114e51e 100644 --- a/services/construction/combine.go +++ b/services/construction/combine.go @@ -18,7 +18,6 @@ import ( "context" "encoding/json" "errors" - "github.com/coinbase/rosetta-sdk-go/types" "github.com/ethereum/go-ethereum/common" ethTypes "github.com/ethereum/go-ethereum/core/types" @@ -43,16 +42,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..b7921f6 100644 --- a/services/construction/combine_test.go +++ b/services/construction/combine_test.go @@ -17,17 +17,17 @@ package construction import ( "context" "encoding/json" - "testing" - "github.com/coinbase/rosetta-sdk-go/types" svcError "github.com/maticnetwork/polygon-rosetta/services/errors" "github.com/stretchr/testify/assert" + "testing" ) + 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..4fa90e8 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,12 +49,17 @@ var ( toAddress = "0xefD3dc58D60aF3295B92ecd484CAEB3A2f30b3e7" tokenContractAddress = "0x2d7882beDcbfDDce29Ba99965dd3cdF7fcB10A1e" + constructionFromAddress = "0x5aCB42b3cfCD734a57AFF800139ba1354b549159" + constructionToAddress = "0x3Fa177c2E87Cb24148EC403921dB577d140CC07c" + transferValue = uint64(20211004) - transferGasPrice = uint64(5000000000) + transferGasPrice = uint64(0) 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) @@ -61,6 +67,26 @@ var ( 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 +139,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 +150,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 +160,44 @@ func TestConstructionFlowWithPendingNonce(t *testing.T) { // Test Metadata metadata := &metadata{ GasLimit: 21000, - GasPrice: big.NewInt(1000000000), + GasPrice: big.NewInt(0), + 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 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( + // "SuggestGasPrice", + // ctx, + // gasPrice, + //).Return( + // big.NewInt(0), + // nil, + //).Once() mockClient.On( "PendingNonceAt", ctx, - common.HexToAddress("0xD10a72Cf054650931365Cc44D912a4FD75257058"), + common.HexToAddress(constructionFromAddress), ).Return( uint64(0), nil, @@ -167,21 +211,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","gas_price":"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 +234,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{ @@ -202,6 +246,8 @@ func TestConstructionFlowWithPendingNonce(t *testing.T) { parseMetadata := &parseMetadata{ Nonce: metadata.Nonce, GasPrice: metadata.GasPrice, + GasCap: metadata.GasCap, + GasTip: metadata.GasTip, GasLimit: metadata.GasLimit, ChainID: big.NewInt(80001), } @@ -212,10 +258,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 +273,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 +283,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 +360,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 +372,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,22 +382,40 @@ func TestConstructionFlowWithInputNonce(t *testing.T) { // Test Metadata metadata := &metadata{ GasLimit: 21000, - GasPrice: big.NewInt(1000000000), + GasPrice: big.NewInt(0), + 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 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( + // "SuggestGasPrice", + // ctx, + // gasPrice, + //).Return( + // big.NewInt(1001000000), + // nil, + //).Once() metadataResponse, err := servicer.ConstructionMetadata(ctx, &types.ConstructionMetadataRequest{ NetworkIdentifier: networkIdentifier, Options: preprocessResponse.Options, @@ -358,21 +425,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","gas_price":"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":"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 +448,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{ @@ -394,6 +461,8 @@ func TestConstructionFlowWithInputNonce(t *testing.T) { 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 +472,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 +487,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 +496,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..7c97a35 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,35 @@ 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 { + multiplier := big.NewInt(2) + gasCap = new(big.Int).Add(gasTip, new(big.Int).Mul(header.BaseFee, multiplier)) + } else { + gasCap = input.GasCap + } + + // gasPrice, err := a.client.SuggestGasPrice(ctx, input.GasPrice) + // if err != nil { + // return nil, svcErrors.WrapErr(svcErrors.ErrGeth, err) + //} + metadata := &metadata{ Nonce: nonce, - GasPrice: gasPrice, + GasPrice: big.NewInt(0), GasLimit: gasLimit, + GasCap: gasCap, + GasTip: gasTip, Data: input.Data, Value: input.Value, To: to, @@ -131,7 +149,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..337f7e1 100644 --- a/services/construction/metadata_test.go +++ b/services/construction/metadata_test.go @@ -39,6 +39,8 @@ var ( metadataGenericData = "0x095ea7b3000000000000000000000000d10a72cf054650931365cc44d912a4fd7525705800000000000000000000000000000000000000000000000000000000000003e8" maticTokenContract = "0x0000000000000000000000000000000000001010" metadataMaticWithdrawData = "0x2e1a7d4d0000000000000000000000000000000000000000000000000000000005f5e100" + + metadataGasPriceHex = "0x0" ) func TestMetadata_Offline(t *testing.T) { @@ -75,21 +77,31 @@ func TestMetadata(t *testing.T) { "to": metadataTo, "value": transferValueHex, "nonce": transferNonceHex2, - "gas_price": transferGasPriceHex, + "gas_price": metadataGasPriceHex, "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) + // var gasPrice *big.Int = nil + + // client.On("SuggestGasPrice", ctx, gasPrice). + // Return(big.NewInt(int64(transferGasPrice)), nil) }, }, "happy path: native currency without nonce": { @@ -99,25 +111,36 @@ 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) + + // var gasPrice *big.Int = nil + + //client.On("SuggestGasPrice", ctx, gasPrice). + // Return(big.NewInt(int64(transferGasTip)), nil) }, expectedResponse: &types.ConstructionMetadataResponse{ Metadata: map[string]interface{}{ "to": metadataTo, "value": transferValueHex, "nonce": transferNonceHex, - "gas_price": transferGasPriceHex, + "gas_price": metadataGasPriceHex, "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 +156,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 +165,33 @@ 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) + + // var gasPrice *big.Int = nil + + // client.On("SuggestGasPrice", ctx, gasPrice). + // Return(big.NewInt(int64(transferGasPrice)), nil) }, expectedResponse: &types.ConstructionMetadataResponse{ Metadata: map[string]interface{}{ "to": tokenContractAddress, "value": "0x0", "nonce": transferNonceHex2, - "gas_price": transferGasPriceHex, + "gas_price": metadataGasPriceHex, "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 +209,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 +218,35 @@ 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) + + // var gasPrice *big.Int = nil + + // client.On("SuggestGasPrice", ctx, gasPrice). + // Return(big.NewInt(int64(transferGasPrice)), nil) }, expectedResponse: &types.ConstructionMetadataResponse{ Metadata: map[string]interface{}{ "to": tokenContractAddress, "value": "0x0", "nonce": transferNonceHex2, - "gas_price": transferGasPriceHex, + "gas_price": metadataGasPriceHex, "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 +264,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 +274,35 @@ func TestMetadata(t *testing.T) { Value: big.NewInt(100000000), }).Return(transferGasLimitERC20, nil) - client.On("SuggestGasPrice", ctx, gasPrice). - Return(big.NewInt(int64(transferGasPrice)), nil) + // var gasPrice *big.Int = 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_price": metadataGasPriceHex, "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..1628826 100644 --- a/services/construction/parse.go +++ b/services/construction/parse.go @@ -18,6 +18,7 @@ import ( "context" "encoding/json" "fmt" + "math/big" "github.com/coinbase/rosetta-sdk-go/types" ethTypes "github.com/ethereum/go-ethereum/core/types" @@ -38,6 +39,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 +49,13 @@ func (a *APIService) ConstructionParse( tx.Value = t.Value() tx.Data = t.Data() tx.Nonce = t.Nonce() - tx.GasPrice = t.GasPrice() + tx.GasPrice = big.NewInt(0) + 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) } @@ -101,6 +105,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 +132,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..ad4ce11 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,29 @@ 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","gas_price":"0x0","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","gas_price":"0x0","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","gas_price":"0x0","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","gas_price":"0x0","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","gas_price":"0x0","max_fee_per_gas":"0x59682f15","max_priority_fee_per_gas":"0x59682eff","gas":"0x5208","chain_id":"0x13881"}` //nolint:lll + + parseFromAddress = "0x5aCB42b3cfCD734a57AFF800139ba1354b549159" + parseToAddress = "0x3Fa177c2E87Cb24148EC403921dB577d140CC07c" + parseTokenContractAddress = "0x2d7882beDcbfDDce29Ba99965dd3cdF7fcB10A1e" + + gasPriceHex = "0x0" + 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 +66,14 @@ 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_price": gasPriceHex, "gas_limit": transferGasLimitHex, + "gas_cap": gasCapHex, + "gas_tip": gasTipHex, "chain_id": chainIDHex, }, }, @@ -66,16 +85,18 @@ 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_price": gasPriceHex, "gas_limit": transferGasLimitHex, + "gas_cap": gasCapHex, + "gas_tip": gasTipHex, "chain_id": chainIDHex, }, }, @@ -87,18 +108,20 @@ 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_price": gasPriceHex, + "gas_limit": gasLimitERC20Hex, + "gas_cap": gasCapERC20Hex, + "gas_tip": gasTipERC20Hex, "chain_id": chainIDHex, }, }, @@ -110,22 +133,24 @@ 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_price": gasPriceHex, + "gas_limit": gasLimitERC20Hex, + "gas_cap": gasCapERC20Hex, + "gas_tip": gasTipERC20Hex, "chain_id": chainIDHex, }, }, @@ -182,3 +207,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..7663fdd 100644 --- a/services/construction/payloads.go +++ b/services/construction/payloads.go @@ -59,6 +59,8 @@ func (a *APIService) ConstructionPayloads( 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 +77,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, @@ -92,11 +97,13 @@ func (a *APIService) ConstructionPayloads( 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..fbde94d 100644 --- a/services/construction/payloads_test.go +++ b/services/construction/payloads_test.go @@ -33,6 +33,8 @@ import ( var ( invalidTransferData = "0xaaaaaaaa000000000000000000000000efd3dc58d60af3295b92ecd484caeb3a2f30b3e70000000000000000000000000000000000000000000000000000000000000001" // nolint + payloadsGasPriceHex = "0x0" + ) func TestPayloads(t *testing.T) { @@ -48,7 +50,7 @@ func TestPayloads(t *testing.T) { ), expectedResponse: templateConstructionPayloadsResponse( templateNativeCurrencyUnsigned(), - "0xa5f38ab5ca0c7c398e90f44111c3aae0c02be5c3054dc8933174b9898a4dcdfd", + "0xc06f3a7b39de0be0497941cade621537240067f068385ecb958357f68e13db7d", ), }, "happy path: ERC20 currency": { @@ -64,7 +66,7 @@ func TestPayloads(t *testing.T) { ), expectedResponse: templateConstructionPayloadsResponse( templateERC20CurrencyUnsigned(), - "0x55bf0447d109c960db3290be9bf3893f7b2476fd71c23e85550dd55b4602ea23", + "0x636590d08514899553fc91879fa49ad56708b63ed3be858c161b57f42e5adbfd", ), }, "happy path: Generic contract call": { @@ -80,7 +82,7 @@ func TestPayloads(t *testing.T) { ), expectedResponse: templateConstructionPayloadsResponse( templateGenericContractCallUnsigned(), - "0x6db5acd2132d1eaa7cf47392b98ecee1befe2a760f2e68d7ef7e9df40f63a384", + "0x17edf049123b31a3d9c537f2a8b705838c2e0346a427f079a593c61a688d2fd3", ), }, "error: bad request: native currency mismatch destination address": { @@ -194,8 +196,10 @@ func templateNativeCurrencyTxMetadata(amount string) map[string]interface{} { "nonce": transferNonceHex, "to": metadataTo, "value": amount, - "gas_price": transferGasPriceHex, + "gas_price": payloadsGasPriceHex, "gas_limit": transferGasLimitHex, + "gas_cap": transferGasCapHex, + "gas_tip": transferGasTipHex, } } @@ -204,21 +208,25 @@ func templateERC20CurrencyTxMetadata() map[string]interface{} { "nonce": transferNonceHex, "to": tokenContractAddress, "value": "0x0", - "gas_price": transferGasPriceHex, + "gas_price": payloadsGasPriceHex, "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","gas_price":"%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, + payloadsGasPriceHex, + transferGasCapHex, + transferGasTipHex, transferGasLimitHex, chainIDHex, ) @@ -226,13 +234,15 @@ 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","gas_price":"%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, + payloadsGasPriceHex, + transferGasCapHex, + transferGasTipHex, transferGasLimitERC20Hex, chainIDHex, ) @@ -243,8 +253,10 @@ func templateGenericContractCallTxMetadata() map[string]interface{} { "nonce": transferNonceHex, "to": tokenContractAddress, "value": "0x0", - "gas_price": transferGasPriceHex, + "gas_price": payloadsGasPriceHex, "gas_limit": transferGasLimitERC20Hex, + "gas_cap": transferGasCapHex, + "gas_tip": transferGasTipHex, "data": metadataGenericData, "method_signature": "approve(address,uint256)", "method_args": []interface{}{"0xD10a72Cf054650931365Cc44D912a4FD75257058", "1000"}, @@ -253,13 +265,15 @@ 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","gas_price":"%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, + payloadsGasPriceHex, + transferGasCapHex, + transferGasTipHex, transferGasLimitERC20Hex, chainIDHex, ) diff --git a/services/construction/preprocess.go b/services/construction/preprocess.go index 79d82e5..def4844 100644 --- a/services/construction/preprocess.go +++ b/services/construction/preprocess.go @@ -95,6 +95,25 @@ func (a *APIService) ConstructionPreprocess( preprocessOutputOptions.Nonce = 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_price if v, ok := request.Metadata["gas_price"]; ok { stringObj, ok := v.(string) diff --git a/services/construction/preprocess_test.go b/services/construction/preprocess_test.go index 4277892..50f48aa 100644 --- a/services/construction/preprocess_test.go +++ b/services/construction/preprocess_test.go @@ -39,6 +39,8 @@ var ( preprocessGasPriceHex = hexutil.EncodeUint64(preprocessGasPrice) preprocessGasLimit = uint64(600000) preprocessGasLimitHex = hexutil.EncodeUint64(preprocessGasLimit) + preprocessGasCap = uint64(5000000000) + preprocessGasCapHex = hexutil.EncodeUint64(preprocessGasCap) preprocessGenericData = "0x095ea7b3000000000000000000000000d10a72cf054650931365cc44d912a4fd7525705800000000000000000000000000000000000000000000000000000000000003e8" methodSignature = "approve(address,uint256)" methodArgs = []string{"0xD10a72Cf054650931365Cc44D912a4FD75257058", "1000"} @@ -239,6 +241,42 @@ func TestPreprocess(t *testing.T) { }, }, }, + "happy path: native currency with gas cap": { + operations: templateOperations(preprocessTransferValue, polygon.Currency), + metadata: map[string]interface{}{ + "gas_cap": "5000000000", + }, + expectedResponse: &types.ConstructionPreprocessResponse{ + Options: map[string]interface{}{ + "from": preprocessFromAddress, + "to": preprocessToAddress, + "value": preprocessTransferValueHex, + "gas_cap": preprocessGasCapHex, + }, + }, + }, + "happy path: ERC20 currency with gas cap": { + operations: templateOperations(preprocessTransferValue, &types.Currency{ + Symbol: "USDC", + Decimals: 18, + Metadata: map[string]interface{}{ + "token_address": preprocessTokenContractAddress, + }, + }), + metadata: map[string]interface{}{ + "gas_cap": "5000000000", + }, + expectedResponse: &types.ConstructionPreprocessResponse{ + Options: map[string]interface{}{ + "from": preprocessFromAddress, + "to": preprocessToAddress, + "value": "0x0", + "token_address": preprocessTokenContractAddress, + "data": preprocessData, + "gas_cap": preprocessGasCapHex, + }, + }, + }, "error: both positive amount": { operations: func() []*types.Operation { operations := templateOperations(preprocessTransferValue, polygon.Currency) @@ -346,6 +384,24 @@ func TestPreprocess(t *testing.T) { expectedError: templateError( svcErrors.ErrInvalidGasLimit, "gas_limit is not a valid gas_limit"), }, + "error: invalid gas cap string": { + operations: templateOperations(preprocessTransferValue, polygon.Currency), + metadata: map[string]interface{}{ + "gas_cap": map[string]string{}, + }, + expectedResponse: nil, + expectedError: templateError( + svcErrors.ErrInvalidGasCap, "map[] is not a valid gas_cap string"), + }, + "error: invalid gas cap": { + operations: templateOperations(preprocessTransferValue, polygon.Currency), + metadata: map[string]interface{}{ + "gas_cap": "gas_cap", + }, + expectedResponse: nil, + expectedError: templateError( + svcErrors.ErrInvalidGasCap, "gas_cap is not a valid gas_cap"), + }, "error: missing token address": { operations: templateOperations(preprocessTransferValue, &types.Currency{ Symbol: "USDC", diff --git a/services/construction/types.go b/services/construction/types.go index b9da02d..d4085cb 100644 --- a/services/construction/types.go +++ b/services/construction/types.go @@ -56,8 +56,12 @@ type Client interface { 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, @@ -80,6 +84,8 @@ type options struct { 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"` } @@ -94,6 +100,8 @@ type optionsWire struct { 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"` } @@ -128,6 +136,14 @@ func (o *options) MarshalJSON() ([]byte, error) { 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) } @@ -183,12 +199,30 @@ func (o *options) UnmarshalJSON(data []byte) error { o.GasLimit = gasLimit } + if len(ow.GasCap) > 0 { + gasCap, err := hexutil.DecodeBig(ow.GasCap) + if err != nil { + return err + } + o.GasCap = gasCap + } + + if len(ow.GasTip) > 0 { + gasTip, err := hexutil.DecodeBig(ow.GasTip) + if err != nil { + return err + } + o.GasTip = gasTip + } + return nil } 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"` @@ -200,6 +234,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"` @@ -212,6 +248,8 @@ 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, @@ -248,7 +286,19 @@ func (m *metadata) UnmarshalJSON(data []byte) error { return err } + 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 @@ -285,6 +335,8 @@ 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"` } @@ -292,6 +344,8 @@ 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"` } @@ -300,6 +354,8 @@ func (p *parseMetadata) MarshalJSON() ([]byte, error) { 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), } @@ -313,6 +369,8 @@ type transaction struct { 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"` } @@ -324,6 +382,8 @@ type transactionWire struct { 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"` } @@ -337,6 +397,8 @@ func (t *transaction) MarshalJSON() ([]byte, error) { 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 +406,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,7 +427,7 @@ func (t *transaction) UnmarshalJSON(data []byte) error { return err } - gasPrice, err := hexutil.DecodeBig(tw.GasPrice) + gasPrice := big.NewInt(0) if err != nil { return err } @@ -374,6 +437,15 @@ func (t *transaction) UnmarshalJSON(data []byte) error { return err } + gasCap, err := hexutil.DecodeBig(tw.GasCap) + if err != nil { + return err + } + gasTip, err := hexutil.DecodeBig(tw.GasTip) + if err != nil { + return err + } + chainID, err := hexutil.DecodeBig(tw.ChainID) if err != nil { return err @@ -386,6 +458,8 @@ func (t *transaction) UnmarshalJSON(data []byte) error { 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..270cc42 100644 --- a/services/errors/errors.go +++ b/services/errors/errors.go @@ -209,6 +209,13 @@ var ( 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", + } ) // WrapErr adds details to the types.Error provided. We use a function