diff --git a/internal/erc8004/signer.go b/internal/erc8004/signer.go index b4eb7f0a..d2a6338d 100644 --- a/internal/erc8004/signer.go +++ b/internal/erc8004/signer.go @@ -68,16 +68,16 @@ func (s *RemoteSigner) GetAddress(ctx context.Context) (common.Address, error) { } // SignTxRequest contains the fields for signing an EIP-1559 transaction. -// chain_id is sent as a JSON integer (u64) to match the Rust remote-signer's -// expected type — sending it as a string causes HTTP 422. +// All numeric fields are sent as JSON integers (u64) to match the Rust +// remote-signer's expected types — sending any of them as strings causes HTTP 422. type SignTxRequest struct { ChainID int64 `json:"chain_id"` To string `json:"to"` - Nonce string `json:"nonce"` - GasLimit string `json:"gas_limit"` - MaxFeePerGas string `json:"max_fee_per_gas"` - MaxPriorityFeePerGas string `json:"max_priority_fee_per_gas"` - Value string `json:"value"` + Nonce uint64 `json:"nonce"` + GasLimit uint64 `json:"gas_limit"` + MaxFeePerGas uint64 `json:"max_fee_per_gas"` + MaxPriorityFeePerGas uint64 `json:"max_priority_fee_per_gas"` + Value uint64 `json:"value"` Data string `json:"data"` } @@ -192,20 +192,20 @@ func (s *RemoteSigner) RemoteTransactOpts(ctx context.Context, addr common.Addre toAddr = tx.To().Hex() } req := SignTxRequest{ - ChainID: chainID.Int64(), - To: toAddr, - Nonce: fmt.Sprintf("%d", tx.Nonce()), - GasLimit: fmt.Sprintf("%d", tx.Gas()), - Value: tx.Value().String(), - Data: "0x" + hex.EncodeToString(tx.Data()), + ChainID: chainID.Int64(), + To: toAddr, + Nonce: tx.Nonce(), + GasLimit: tx.Gas(), + Value: tx.Value().Uint64(), + Data: "0x" + hex.EncodeToString(tx.Data()), } // Use EIP-1559 fields if available, otherwise legacy gas price. if tx.GasFeeCap() != nil && tx.GasFeeCap().Sign() > 0 { - req.MaxFeePerGas = tx.GasFeeCap().String() - req.MaxPriorityFeePerGas = tx.GasTipCap().String() + req.MaxFeePerGas = tx.GasFeeCap().Uint64() + req.MaxPriorityFeePerGas = tx.GasTipCap().Uint64() } else if tx.GasPrice() != nil { - req.MaxFeePerGas = tx.GasPrice().String() - req.MaxPriorityFeePerGas = tx.GasPrice().String() + req.MaxFeePerGas = tx.GasPrice().Uint64() + req.MaxPriorityFeePerGas = tx.GasPrice().Uint64() } signedHex, err := s.SignTransaction(ctx, fromAddr, req) diff --git a/internal/erc8004/signer_test.go b/internal/erc8004/signer_test.go index fb52b158..b17883a9 100644 --- a/internal/erc8004/signer_test.go +++ b/internal/erc8004/signer_test.go @@ -11,6 +11,7 @@ import ( "testing" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" ) func TestRemoteSigner_GetAddress(t *testing.T) { @@ -73,11 +74,11 @@ func TestRemoteSigner_SignTransaction(t *testing.T) { signed, err := signer.SignTransaction(context.Background(), addr, SignTxRequest{ ChainID: 84532, To: "0x8004A818BFB912233c491871b3d84c89A494BD9e", - Nonce: "0", - GasLimit: "100000", - MaxFeePerGas: "1000000000", - MaxPriorityFeePerGas: "1000000", - Value: "0", + Nonce: 0, + GasLimit: 100000, + MaxFeePerGas: 1000000000, + MaxPriorityFeePerGas: 1000000, + Value: 0, Data: "0x", }) if err != nil { @@ -213,16 +214,19 @@ func TestRemoteSigner_GetAddress_HTTPError(t *testing.T) { func TestRemoteTransactOpts(t *testing.T) { addr := common.HexToAddress("0x1234567890abcdef1234567890abcdef12345678") + to := common.HexToAddress("0x8004A818BFB912233c491871b3d84c89A494BD9e") chainID := big.NewInt(84532) + var body map[string]json.RawMessage + var path string srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - // This verifies the signer receives proper requests. - if r.URL.Path == "/api/v1/keys" { - json.NewEncoder(w).Encode(keysResponse{Keys: []string{addr.Hex()}}) - return + if r.Method != http.MethodPost { + t.Errorf("expected POST, got %s", r.Method) + } + path = r.URL.Path + if err := json.NewDecoder(r.Body).Decode(&body); err != nil { + t.Fatalf("decode request body: %v", err) } - // For transaction signing, return the error since we can't easily - // produce a valid signed tx in a unit test. json.NewEncoder(w).Encode(signResponse{Error: "test: not implemented"}) })) defer srv.Close() @@ -236,6 +240,86 @@ func TestRemoteTransactOpts(t *testing.T) { if opts.Signer == nil { t.Fatal("Signer should not be nil") } + + tx := types.NewTx(&types.DynamicFeeTx{ + ChainID: chainID, + Nonce: 7, + To: &to, + Gas: 100000, + GasFeeCap: big.NewInt(1000000000), + GasTipCap: big.NewInt(1000000), + Value: big.NewInt(12345), + Data: []byte{0xde, 0xad, 0xbe, 0xef}, + }) + + _, err := opts.Signer(addr, tx) + if err == nil { + t.Fatal("expected signer error") + } + if !strings.Contains(err.Error(), "test: not implemented") { + t.Fatalf("unexpected signer error: %v", err) + } + + if path != "/api/v1/sign/"+addr.Hex()+"/transaction" { + t.Fatalf("unexpected request path: %s", path) + } + + assertJSONInt64(t, body, "chain_id", 84532) + assertJSONUint64(t, body, "nonce", 7) + assertJSONUint64(t, body, "gas_limit", 100000) + assertJSONUint64(t, body, "max_fee_per_gas", 1000000000) + assertJSONUint64(t, body, "max_priority_fee_per_gas", 1000000) + assertJSONUint64(t, body, "value", 12345) + assertJSONString(t, body, "to", to.Hex()) + assertJSONString(t, body, "data", "0xdeadbeef") +} + +func assertJSONInt64(t *testing.T, body map[string]json.RawMessage, field string, want int64) { + t.Helper() + raw, ok := body[field] + if !ok { + t.Fatalf("missing field %q", field) + } + + var got int64 + if err := json.Unmarshal(raw, &got); err != nil { + t.Fatalf("field %q should be a JSON integer, got %s: %v", field, string(raw), err) + } + if got != want { + t.Fatalf("field %q = %d, want %d", field, got, want) + } +} + +func assertJSONUint64(t *testing.T, body map[string]json.RawMessage, field string, want uint64) { + t.Helper() + raw, ok := body[field] + if !ok { + t.Fatalf("missing field %q", field) + } + + var got uint64 + if err := json.Unmarshal(raw, &got); err != nil { + t.Fatalf("field %q should be a JSON integer, got %s: %v", field, string(raw), err) + } + if got != want { + t.Fatalf("field %q = %d, want %d", field, got, want) + } +} + +func assertJSONString(t *testing.T, body map[string]json.RawMessage, field, want string) { + t.Helper() + raw, ok := body[field] + if !ok { + t.Fatalf("missing field %q", field) + } + + var got string + if err := json.Unmarshal(raw, &got); err != nil { + t.Fatalf("field %q should be a JSON string, got %s: %v", field, string(raw), err) + } + if got != want { + t.Fatalf("field %q = %q, want %q", field, got, want) + } } func TestHexToBytes(t *testing.T) {