diff --git a/services/construction/construction_service_test.go b/services/construction/construction_service_test.go index 6b18bc2..9955fea 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" + "fmt" "math/big" "testing" @@ -58,8 +59,9 @@ var ( transferGasLimitERC20 = uint64(65000) transferNonce = uint64(67) transferData = "0xa9059cbb000000000000000000000000efd3dc58d60af3295b92ecd484caeb3a2f30b3e7000000000000000000000000000000000000000000000000000000000134653c" //nolint - transferGasCap = uint64(1001000000) - transferGasTip = uint64(1000000000) + transferGasCap = uint64(60000000000) // 60 gwei + transferGasTip = uint64(1500000000) // 1.5 gwei + transferGasCapWithTip = transferGasCap + transferGasTip transferValueHex = hexutil.EncodeUint64(transferValue) transferGasLimitHex = hexutil.EncodeUint64(transferGasLimit) @@ -68,6 +70,10 @@ var ( transferNonceHex2 = "0x22" transferGasCapHex = hexutil.EncodeUint64(transferGasCap) transferGasTipHex = hexutil.EncodeUint64(transferGasTip) + transferGasCapWithTipHex = hexutil.EncodeUint64(transferGasCapWithTip) + + minGasCap = big.NewInt(30000000000) + minGasCapHex = hexutil.EncodeUint64(minGasCap.Uint64()) header = EthTypes.Header{ ParentHash: common.Hash{}, @@ -83,7 +89,24 @@ var ( GasUsed: 0, Time: 0, Extra: hexutil.Bytes{}, - BaseFee: big.NewInt(30000000000), // equivalent to 30 gwei + BaseFee: minGasCap, // equivalent to 30 gwei, previously 500000 + } + + headerWithLowBaseFee = 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(10000000000), // equivalent to 10 gwei } ) @@ -158,15 +181,14 @@ func TestConstructionFlowWithPendingNonce(t *testing.T) { // Test Metadata metadata := &metadata{ GasLimit: 21000, - GasTip: big.NewInt(1500000000), - GasCap: big.NewInt(1501000000), + GasTip: big.NewInt(int64(transferGasTip)), + GasCap: big.NewInt(int64(transferGasCapWithTip)), // math: gasCap = new(big.Int).Add(gasTip, new(big.Int).Mul(baseFee, multiplier)) Nonce: 0, To: constructionToAddress, Value: big.NewInt(1000), } var blockNum *big.Int = nil - mockClient.On( "BlockHeader", ctx, @@ -179,7 +201,7 @@ func TestConstructionFlowWithPendingNonce(t *testing.T) { "SuggestGasTipCap", ctx, ).Return( - big.NewInt(1500000000), + big.NewInt(int64(transferGasTip)), nil, ).Once() mockClient.On( @@ -199,23 +221,27 @@ func TestConstructionFlowWithPendingNonce(t *testing.T) { Metadata: forceMarshalMap(t, metadata), SuggestedFee: []*types.Amount{ { - Value: "661500000000000", + Value: "1291500000000000", Currency: polygon.Currency, }, }, }, metadataResponse) // Test Payloads - unsignedRaw := `{"from":"0x5aCB42b3cfCD734a57AFF800139ba1354b549159","to":"0x3Fa177c2E87Cb24148EC403921dB577d140CC07c","value":"0x3e8","data":"0x","nonce":"0x0","max_fee_per_gas":"0x59682f00","max_priority_fee_per_gas":"0x59682f00","gas":"0x5208","chain_id":"0x13881"}` + unsignedRaw := `{"from":"0x5aCB42b3cfCD734a57AFF800139ba1354b549159","to":"0x3Fa177c2E87Cb24148EC403921dB577d140CC07c","value":"0x3e8","data":"0x","nonce":"0x0","max_fee_per_gas":"0xe51af8700","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":"0x5aCB42b3cfCD734a57AFF800139ba1354b549159","hex_bytes":"9df2732e3102b2de6c837eb1055292b1f0472b6ae898dff7ba917afc61719120","account_identifier":{"address":"0x5aCB42b3cfCD734a57AFF800139ba1354b549159"},"signature_type":"ecdsa_recovery"}]` + + payloadsRaw := `[{"address":"0x5aCB42b3cfCD734a57AFF800139ba1354b549159","hex_bytes":"0206e22e9bded068a76f89a86e0849b7e6ff8f6e8a22e1b679fd87a08635a9f2","account_identifier":{"address":"0x5aCB42b3cfCD734a57AFF800139ba1354b549159"},"signature_type":"ecdsa_recovery"}]` var payloads []*types.SigningPayload assert.NoError(t, json.Unmarshal([]byte(payloadsRaw), &payloads)) + + hexString := hexutil.Encode(payloadsResponse.Payloads[0].Bytes) + fmt.Printf("hexstring %s", hexString) assert.Equal(t, &types.ConstructionPayloadsResponse{ UnsignedTransaction: unsignedRaw, Payloads: payloads, @@ -245,15 +271,18 @@ func TestConstructionFlowWithPendingNonce(t *testing.T) { }, parseUnsignedResponse) // Test Combine - signaturesRaw := `[{"hex_bytes":"9f2f61a9a90f6695b10ed04102dca3e0aa50a10263afd861761226e3e61903a62fb42a9e8d96af626e64fd20573338f41d4f90ea9834ce6aa4ff79869181699700","public_key":{"hex_bytes":"0405e82ac561143aafc13ba109677a597c8f797b07417d0addd7a346ad35882b3c4a006620e02127b9a32e90979ff93ecad0a2f577db238163a50023e393e354ff","curve_type":"secp256k1"},"signing_payload":{"hex_bytes":"9df2732e3102b2de6c837eb1055292b1f0472b6ae898dff7ba917afc61719120","address":"0x5aCB42b3cfCD734a57AFF800139ba1354b549159"},"signature_type":"ecdsa_recovery"}]` + signaturesRaw := `[{"hex_bytes":"9f2f61a9a90f6695b10ed04102dca3e0aa50a10263afd861761226e3e61903a62fb42a9e8d96af626e64fd20573338f41d4f90ea9834ce6aa4ff79869181699700","public_key":{"hex_bytes":"0405e82ac561143aafc13ba109677a597c8f797b07417d0addd7a346ad35882b3c4a006620e02127b9a32e90979ff93ecad0a2f577db238163a50023e393e354ff","curve_type":"secp256k1"},"signing_payload":{"hex_bytes":"0206e22e9bded068a76f89a86e0849b7e6ff8f6e8a22e1b679fd87a08635a9f2","address":"0x5aCB42b3cfCD734a57AFF800139ba1354b549159"},"signature_type":"ecdsa_recovery"}]` var signatures []*types.Signature assert.NoError(t, json.Unmarshal([]byte(signaturesRaw), &signatures)) - signedRaw := `{"type":"0x2","nonce":"0x0","gasPrice":null,"maxPriorityFeePerGas":"0x59682f00","maxFeePerGas":"0xe51af8700","gas":"0x5208","value":"0x3e8","input":"0x","v":"0x0","r":"0x9f2f61a9a90f6695b10ed04102dca3e0aa50a10263afd861761226e3e61903a6","s":"0x2fb42a9e8d96af626e64fd20573338f41d4f90ea9834ce6aa4ff798691816997","to":"0x3fa177c2e87cb24148ec403921db577d140cc07c","chainId":"0x13881","accessList":[],"hash":"0xfacf81ceb293b34292ea428c64a4a550fd5702432908a952aa9d1db455c22c72"}` //nolint + signedRaw := `{"type":"0x2","nonce":"0x0","gasPrice":null,"maxPriorityFeePerGas":"0x59682f00","maxFeePerGas":"0xe51af8700","gas":"0x5208","value":"0x3e8","input":"0x","v":"0x0","r":"0x9f2f61a9a90f6695b10ed04102dca3e0aa50a10263afd861761226e3e61903a6","s":"0x2fb42a9e8d96af626e64fd20573338f41d4f90ea9834ce6aa4ff798691816997","to":"0x3fa177c2e87cb24148ec403921db577d140cc07c","chainId":"0x13881","accessList":[],"hash":"0xa7c55d7deafc3a717c36162b1dad13b7738ff5898dce3c946dd879e2e73ce840"}` //nolint combineResponse, err := servicer.ConstructionCombine(ctx, &types.ConstructionCombineRequest{ NetworkIdentifier: networkIdentifier, UnsignedTransaction: unsignedRaw, Signatures: signatures, }) + + fmt.Printf("resulting payload: %v\n", combineResponse.SignedTransaction) + assert.Nil(t, err) assert.Equal(t, &types.ConstructionCombineResponse{ SignedTransaction: signedRaw, diff --git a/services/construction/metadata.go b/services/construction/metadata.go index d187493..54e1e62 100644 --- a/services/construction/metadata.go +++ b/services/construction/metadata.go @@ -117,25 +117,23 @@ func (a *APIService) ConstructionMetadata( return nil, svcErrors.WrapErr(svcErrors.ErrGeth, err) } - // Initially set baseFee to 30 gwei, the minimum gas price that the node will accept on mainnet. - // See https://forum.polygon.technology/t/recommended-min-gas-price-setting/7604 for additional context. - baseFee := big.NewInt(30000000000) - - // get the maximum of header.BaseFee and 30 gwei - if header.BaseFee.Cmp(baseFee) == 1 { - baseFee = header.BaseFee - } - 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 + // 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(baseFee, multiplier)) + gasCap = new(big.Int).Add(gasTip, new(big.Int).Mul(header.BaseFee, multiplier)) } else { gasCap = input.GasCap } + // Ensure the gas cap is at least 30 gwei, the minimum gas price that the node will accept on mainnet. + // See https://forum.polygon.technology/t/recommended-min-gas-price-setting/7604 for additional context. + minFee := big.NewInt(30000000000) // 30 gwei + if minFee.Cmp(gasCap) == 1 { + gasCap = minFee + } + metadata := &metadata{ Nonce: nonce, GasLimit: gasLimit, @@ -153,8 +151,9 @@ func (a *APIService) ConstructionMetadata( return nil, svcErrors.WrapErr(svcErrors.ErrUnableToParseIntermediateResult, err) } - // Find suggested gas usage - suggestedFee := (baseFee.Int64() + metadata.GasTip.Int64()) * int64(gasLimit) + // Find suggested gas usage. Note that this figure accounts for the minimum (30 gwei), and + // is potentially doubled the block base fee. + suggestedFee := gasCap.Int64() * int64(gasLimit) return &types.ConstructionMetadataResponse{ Metadata: metadataMap, diff --git a/services/construction/metadata_test.go b/services/construction/metadata_test.go index 60d62a4..e2f59a9 100644 --- a/services/construction/metadata_test.go +++ b/services/construction/metadata_test.go @@ -76,12 +76,12 @@ func TestMetadata(t *testing.T) { "value": transferValueHex, "nonce": transferNonceHex2, "gas_limit": transferGasLimitHex, - "gas_cap": transferGasCapHex, + "gas_cap": transferGasCapWithTipHex, "gas_tip": transferGasTipHex, }, SuggestedFee: []*types.Amount{ { - Value: fmt.Sprintf("%d", (header.BaseFee.Uint64()+transferGasTip)*transferGasLimit), + Value: fmt.Sprintf("%d", transferGasCapWithTip*transferGasLimit), Currency: polygon.Currency, }, }, @@ -123,17 +123,51 @@ func TestMetadata(t *testing.T) { "value": transferValueHex, "nonce": transferNonceHex, "gas_limit": transferGasLimitHex, - "gas_cap": transferGasCapHex, + "gas_cap": transferGasCapWithTipHex, "gas_tip": transferGasTipHex, }, SuggestedFee: []*types.Amount{ { - Value: fmt.Sprintf("%d", (header.BaseFee.Uint64()+transferGasTip)*transferGasLimit), + Value: fmt.Sprintf("%d", transferGasCapWithTip*transferGasLimit), Currency: polygon.Currency, }, }, }, }, + "happy path: native currency with gas tip set to 30 gwei floor": { + options: map[string]interface{}{ + "from": metadataFrom, + "to": metadataTo, + "value": transferValueHex, + "nonce": transferNonceHex2, + }, + expectedResponse: &types.ConstructionMetadataResponse{ + Metadata: map[string]interface{}{ + "to": metadataTo, + "value": transferValueHex, + "nonce": transferNonceHex2, + "gas_limit": transferGasLimitHex, + "gas_cap": minGasCapHex, + "gas_tip": transferGasTipHex, + }, + SuggestedFee: []*types.Amount{ + { + Value: fmt.Sprintf("%d", (minGasCap.Uint64())*transferGasLimit), + Currency: polygon.Currency, + }, + }, + }, + mocks: func(ctx context.Context, client *mocks.Client) { + var blockNum *big.Int = nil + + client.On("BlockHeader", ctx, blockNum). + Return(&headerWithLowBaseFee, nil) + + client.On("SuggestGasTipCap", ctx). + Return(big.NewInt(int64(transferGasTip)), nil) + + }, + }, "happy path: ERC20 currency with nonce": { options: map[string]interface{}{ "from": metadataFrom, @@ -168,13 +202,13 @@ func TestMetadata(t *testing.T) { "value": "0x0", "nonce": transferNonceHex2, "gas_limit": transferGasLimitERC20Hex, - "gas_cap": transferGasCapHex, + "gas_cap": transferGasCapWithTipHex, "gas_tip": transferGasTipHex, "data": metadataData, }, SuggestedFee: []*types.Amount{ { - Value: fmt.Sprintf("%d", (header.BaseFee.Uint64()+transferGasTip)*transferGasLimitERC20), + Value: fmt.Sprintf("%d", transferGasCapWithTip*transferGasLimitERC20), Currency: polygon.Currency, }, }, @@ -216,7 +250,7 @@ func TestMetadata(t *testing.T) { "value": "0x0", "nonce": transferNonceHex2, "gas_limit": transferGasLimitERC20Hex, - "gas_cap": transferGasCapHex, + "gas_cap": transferGasCapWithTipHex, "gas_tip": transferGasTipHex, "data": metadataGenericData, "method_signature": "approve(address,uint256)", @@ -224,7 +258,7 @@ func TestMetadata(t *testing.T) { }, SuggestedFee: []*types.Amount{ { - Value: fmt.Sprintf("%d", (header.BaseFee.Uint64()+transferGasTip)*transferGasLimitERC20), + Value: fmt.Sprintf("%d", transferGasCapWithTip*transferGasLimitERC20), Currency: polygon.Currency, }, }, @@ -266,7 +300,7 @@ func TestMetadata(t *testing.T) { "value": "0x5f5e100", "nonce": transferNonceHex2, "gas_limit": transferGasLimitERC20Hex, - "gas_cap": transferGasCapHex, + "gas_cap": transferGasCapWithTipHex, "gas_tip": transferGasTipHex, "data": metadataMaticWithdrawData, "method_signature": "withdraw(uint256)", @@ -274,7 +308,7 @@ func TestMetadata(t *testing.T) { }, SuggestedFee: []*types.Amount{ { - Value: fmt.Sprintf("%d", (header.BaseFee.Uint64()+transferGasTip)*transferGasLimitERC20), + Value: fmt.Sprintf("%d", transferGasCapWithTip*transferGasLimitERC20), Currency: polygon.Currency, }, },